新增设备总览透光率产线产品监控等
This commit is contained in:
563
src/views/equipment/equipmentOverview/alarm-or-updata.vue
Normal file
563
src/views/equipment/equipmentOverview/alarm-or-updata.vue
Normal file
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="80%" @close="handleClose" title-class="dialog-title">
|
||||
<small-title slot="title" :no-padding="true">
|
||||
{{ dataForm.lineId + '·' + dataForm.equipmentName }}
|
||||
</small-title>
|
||||
<search-bar removeBlue :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
|
||||
<el-tabs class="custom-tabs" v-model="activeLabel" :stretch="true" @tab-click="handleTabClick">
|
||||
<el-tab-pane :label="'\u3000报警时长\u3000'" name="duration"></el-tab-pane>
|
||||
<el-tab-pane :label="'\u3000报警次数\u3000'" name="times"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="content">
|
||||
<div class="visual-part">
|
||||
<div v-if="hasData" style="display: flex; justify-content: space-around; gap: 20px; padding: 10px 0;">
|
||||
<!-- 移除 v-if,始终渲染两个图表容器 -->
|
||||
<div id="barChart" style="width: 48%; height: 400px;"></div>
|
||||
<div id="pieChart" style="width: 48%; height: 400px;"></div>
|
||||
</div>
|
||||
<div v-if="!hasData" class="no-data">
|
||||
<el-empty description="暂无相关报警数据"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAlarmDet } from '@/api/base/equipment';
|
||||
import * as echarts from 'echarts';
|
||||
import SmallTitle from './SmallTitle';
|
||||
|
||||
const CHART_CONFIG = {
|
||||
barColor: '#288AFF',
|
||||
pieColors: [
|
||||
'#288AFF', '#4096FF', '#69B1FF', '#91CFFF', '#B8E0FF',
|
||||
'#E0F2FF', '#1890FF', '#096DD9', '#0050B3', '#003A8C'
|
||||
],
|
||||
fontColor: '#333',
|
||||
lightFontColor: '#666',
|
||||
borderRadius: 4
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { SmallTitle },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
hasData: false,
|
||||
listQuery: {
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
equipmentId: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined
|
||||
},
|
||||
formConfig: [
|
||||
{
|
||||
type: 'datePicker',
|
||||
label: '时间段',
|
||||
dateType: 'daterange',
|
||||
format: 'yyyy-MM-dd',
|
||||
valueFormat: 'timestamp',
|
||||
rangeSeparator: '-',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
param: 'timeVal',
|
||||
defaultTime: ['00:00:00', '23:59:59'],
|
||||
defaultSelect: []
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
btnName: '查询',
|
||||
name: 'search',
|
||||
color: 'primary'
|
||||
}
|
||||
],
|
||||
activeLabel: 'duration', // 默认选中「报警时长」
|
||||
dataForm: {
|
||||
equipmentId: undefined,
|
||||
equipmentName: undefined,
|
||||
lineId: undefined
|
||||
},
|
||||
chartInstances: {
|
||||
bar: null,
|
||||
pie: null
|
||||
},
|
||||
isDomReady: false,
|
||||
originData: null // 存储原始数据
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.isDomReady = true;
|
||||
if (this.listQuery.equipmentId) {
|
||||
this.getDataList();
|
||||
}
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
// Tab 切换时自动刷新图表(无需额外操作,依赖 handleTabClick 触发查询)
|
||||
activeLabel() {
|
||||
if (this.isDomReady && this.originData) {
|
||||
this.$nextTick(() => {
|
||||
this.renderBothCharts(); // 切换 Tab 后重新渲染两个图表
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initDefaultDate() {
|
||||
const today = new Date();
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0).getTime();
|
||||
const end = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 0).getTime();
|
||||
|
||||
this.formConfig[0].defaultSelect = [start, end];
|
||||
this.listQuery.startTime = start;
|
||||
this.listQuery.endTime = end;
|
||||
|
||||
// 修复点1:增加多层判断,确保对象存在后再赋值
|
||||
if (this.$refs.searchBarForm && this.$refs.searchBarForm.form) {
|
||||
this.$refs.searchBarForm.form.timeVal = [start, end];
|
||||
}
|
||||
},
|
||||
|
||||
handleTabClick() {
|
||||
// 切换 Tab 时重新查询数据(或直接复用已有数据渲染)
|
||||
this.getDataList();
|
||||
},
|
||||
|
||||
buttonClick(val) {
|
||||
switch (val.btnName) {
|
||||
case 'search':
|
||||
this.listQuery.startTime = val.timeVal?.[0];
|
||||
this.listQuery.endTime = val.timeVal?.[1];
|
||||
this.getDataList();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
|
||||
async getDataList() {
|
||||
try {
|
||||
if (!this.listQuery.equipmentId) {
|
||||
console.warn('设备ID不能为空');
|
||||
this.hasData = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
equipmentId: this.listQuery.equipmentId,
|
||||
startTime: this.listQuery.startTime,
|
||||
endTime: this.listQuery.endTime,
|
||||
};
|
||||
|
||||
const res = await getAlarmDet(queryParams);
|
||||
const originData = res.data || [];
|
||||
this.originData = originData;
|
||||
this.hasData = originData.length > 0;
|
||||
|
||||
if (this.hasData && this.isDomReady) {
|
||||
this.$nextTick(() => {
|
||||
this.renderBothCharts(); // 数据查询成功后,同时渲染两个图表
|
||||
});
|
||||
} else {
|
||||
this.destroyAllCharts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取报警数据失败:', error);
|
||||
this.hasData = false;
|
||||
this.destroyAllCharts();
|
||||
}
|
||||
},
|
||||
|
||||
// 核心方法:同时渲染柱状图和饼图(根据当前 Tab 类型)
|
||||
renderBothCharts() {
|
||||
if (this.activeLabel === 'duration') {
|
||||
// 报警时长:柱状图(时长排序)+ 饼图(时长占比)
|
||||
this.renderBarChart('duration');
|
||||
this.renderPieChart('duration');
|
||||
} else {
|
||||
// 报警次数:柱状图(次数排序)+ 饼图(次数占比)
|
||||
this.renderBarChart('times');
|
||||
this.renderPieChart('times');
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染柱状图(支持两种数据类型)
|
||||
renderBarChart(type) {
|
||||
this.destroyChart('bar');
|
||||
const chartDom = document.getElementById('barChart');
|
||||
if (!chartDom || !this.originData.length) return;
|
||||
|
||||
// 根据类型排序和提取数据
|
||||
let sortedData, xData, seriesData, yAxisName;
|
||||
if (type === 'duration') {
|
||||
// 报警时长:按时长降序
|
||||
sortedData = [...this.originData].sort((a, b) => b.alarmDuration - a.alarmDuration);
|
||||
seriesData = sortedData.map(item => item.alarmDuration);
|
||||
yAxisName = '报警时长';
|
||||
} else {
|
||||
// 报警次数:按次数降序
|
||||
sortedData = [...this.originData].sort((a, b) => b.alarmCount - a.alarmCount);
|
||||
seriesData = sortedData.map(item => item.alarmCount);
|
||||
yAxisName = '报警次数';
|
||||
}
|
||||
|
||||
xData = sortedData.map(item => this.truncateText(item.alarmContent, 8));
|
||||
|
||||
try {
|
||||
this.chartInstances.bar = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
text: `${yAxisName}统计(柱状图)`,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14, color: CHART_CONFIG.fontColor }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
padding: 10,
|
||||
textStyle: { fontSize: 11 },
|
||||
formatter: (params) => {
|
||||
const index = params[0].dataIndex;
|
||||
const item = sortedData[index];
|
||||
return `
|
||||
<div style="text-align: left;">
|
||||
<div>${item.alarmContent}</div>
|
||||
<div>${yAxisName}:${type === 'duration' ? item.alarmDuration : item.alarmCount}</div>
|
||||
<div>占比:${type === 'duration' ? item.alarmDurationRatio.toFixed(2) : item.alarmCountRatio.toFixed(2)}%</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '18%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: xData,
|
||||
axisTick: { alignWithLabel: true },
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
fontSize: 12,
|
||||
color: CHART_CONFIG.lightFontColor
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#e8e8e8' } }
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: yAxisName,
|
||||
nameTextStyle: { fontSize: 11, color: CHART_CONFIG.lightFontColor },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
color: CHART_CONFIG.lightFontColor,
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
splitLine: { lineStyle: { color: '#f5f5f5' } },
|
||||
max: (value) => value.max * 1.2
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: yAxisName,
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: CHART_CONFIG.barColor,
|
||||
borderRadius: [CHART_CONFIG.borderRadius, CHART_CONFIG.borderRadius, 0, 0],
|
||||
shadowBlur: 3,
|
||||
shadowColor: 'rgba(40, 138, 255, 0.2)',
|
||||
shadowOffsetY: 2
|
||||
},
|
||||
barWidth: '16',
|
||||
data: seriesData,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 6,
|
||||
fontSize: 11,
|
||||
color: CHART_CONFIG.fontColor,
|
||||
formatter: (params) => `${params.value}`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.chartInstances.bar.setOption(option);
|
||||
this.addResizeListener('bar');
|
||||
} catch (error) {
|
||||
console.error(`${yAxisName}柱状图初始化失败:`, error);
|
||||
setTimeout(() => this.renderBarChart(type), 200);
|
||||
}
|
||||
},
|
||||
|
||||
// 渲染饼图(支持两种数据类型)
|
||||
renderPieChart(type) {
|
||||
this.destroyChart('pie');
|
||||
const chartDom = document.getElementById('pieChart');
|
||||
if (!chartDom || !this.originData.length) return;
|
||||
|
||||
// 根据类型处理饼图数据
|
||||
let pieData, seriesName;
|
||||
if (type === 'duration') {
|
||||
// 报警时长:按时长占比处理
|
||||
seriesName = '报警时长';
|
||||
pieData = this.handlePieData(this.originData, 'alarmDuration', 'alarmDurationRatio');
|
||||
} else {
|
||||
// 报警次数:按次数占比处理
|
||||
seriesName = '报警次数';
|
||||
pieData = this.handlePieData(this.originData, 'alarmCount', 'alarmCountRatio');
|
||||
}
|
||||
|
||||
try {
|
||||
this.chartInstances.pie = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
text: `${seriesName}统计(饼图)`,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14, color: CHART_CONFIG.fontColor }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
padding: 10,
|
||||
textStyle: { fontSize: 11 },
|
||||
formatter: (params) => {
|
||||
return `
|
||||
<div style="text-align: left;">
|
||||
<div>${params.name}</div>
|
||||
<div>${seriesName}:${params.value}${type === 'duration' ? '' : '次'}</div>
|
||||
<div>占比:${params.percent.toFixed(2)}%</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: seriesName,
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '55%'],
|
||||
color: CHART_CONFIG.pieColors,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
distance: 15,
|
||||
fontSize: 11,
|
||||
color: CHART_CONFIG.lightFontColor,
|
||||
formatter: (params) => {
|
||||
const truncatedName = this.truncateText(params.name, 8);
|
||||
return `${truncatedName}(${params.value}${type === 'duration' ? '' : '次'}, ${params.percent.toFixed(1)}%)`;
|
||||
},
|
||||
align: 'center',
|
||||
baseline: 'middle'
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 15,
|
||||
length2: 20,
|
||||
lineStyle: {
|
||||
color: '#ccc',
|
||||
width: 1,
|
||||
type: 'solid'
|
||||
},
|
||||
smooth: 0.2
|
||||
},
|
||||
data: pieData,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
label: {
|
||||
color: CHART_CONFIG.fontColor,
|
||||
fontSize: 12,
|
||||
fontWeight: 500
|
||||
},
|
||||
labelLine: {
|
||||
lineStyle: {
|
||||
color: CHART_CONFIG.barColor,
|
||||
width: 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.chartInstances.pie.setOption(option);
|
||||
this.addResizeListener('pie');
|
||||
} catch (error) {
|
||||
console.error(`${seriesName}饼图初始化失败:`, error);
|
||||
setTimeout(() => this.renderPieChart(type), 200);
|
||||
}
|
||||
},
|
||||
|
||||
// 通用饼图数据处理(支持动态字段)
|
||||
handlePieData(data, valueKey, ratioKey) {
|
||||
const threshold = 5; // 占比低于5%合并为「其他」
|
||||
let otherCount = 0;
|
||||
const mainData = data.filter(item => {
|
||||
if (item[ratioKey] >= threshold) {
|
||||
return true;
|
||||
} else {
|
||||
otherCount += item[valueKey];
|
||||
return false;
|
||||
}
|
||||
}).map(item => ({
|
||||
name: item.alarmContent,
|
||||
value: item[valueKey],
|
||||
ratio: item[ratioKey]
|
||||
}));
|
||||
|
||||
if (otherCount > 0) {
|
||||
mainData.push({
|
||||
name: '其他',
|
||||
value: otherCount,
|
||||
ratio: 100 - mainData.reduce((sum, item) => sum + item.ratio, 0)
|
||||
});
|
||||
}
|
||||
|
||||
return mainData;
|
||||
},
|
||||
|
||||
truncateText(text, maxLength) {
|
||||
if (!text) return '';
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
|
||||
},
|
||||
|
||||
addResizeListener(type) {
|
||||
const chart = this.chartInstances[type];
|
||||
if (chart) {
|
||||
const resizeHandler = () => chart.resize();
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
chart.resizeHandler = resizeHandler;
|
||||
}
|
||||
},
|
||||
|
||||
destroyChart(type) {
|
||||
const chart = this.chartInstances[type];
|
||||
if (chart) {
|
||||
window.removeEventListener('resize', chart.resizeHandler);
|
||||
chart.dispose();
|
||||
this.chartInstances[type] = null;
|
||||
}
|
||||
},
|
||||
|
||||
destroyAllCharts() {
|
||||
Object.keys(this.chartInstances).forEach(type => {
|
||||
this.destroyChart(type);
|
||||
});
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.destroyAllCharts();
|
||||
this.formConfig[0].defaultSelect = [];
|
||||
this.listQuery.startTime = undefined;
|
||||
this.listQuery.endTime = undefined;
|
||||
this.originData = null;
|
||||
this.hasData = true;
|
||||
// 修复点2:增加多层判断,确保对象存在后再赋值
|
||||
if (this.$refs.searchBarForm && this.$refs.searchBarForm.form) {
|
||||
this.$refs.searchBarForm.form.timeVal = [];
|
||||
}
|
||||
},
|
||||
|
||||
init(data) {
|
||||
this.dataForm = {
|
||||
equipmentId: data.equipmentId || '',
|
||||
equipmentName: data.equipmentName || '',
|
||||
lineId: data.lineId || ''
|
||||
};
|
||||
this.activeLabel = 'duration'
|
||||
this.listQuery.equipmentId = data.equipmentId || undefined;
|
||||
this.visible = true;
|
||||
this.originData = null;
|
||||
this.hasData = false;
|
||||
|
||||
this.initDefaultDate();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.isDomReady = true;
|
||||
this.getDataList();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.destroyAllCharts();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式,优化图表容器布局 */
|
||||
.drawer>>>.el-drawer {
|
||||
border-radius: 8px 0 0 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawer>>>.el-form-item__label {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawer>>>.el-drawer__header {
|
||||
margin: 0;
|
||||
padding: 32px 32px 24px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.drawer>>>.el-drawer__body {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawer>>>.content {
|
||||
padding: 30px 24px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawer>>>.visual-part {
|
||||
flex: 1 auto;
|
||||
max-height: 76vh;
|
||||
overflow: hidden;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* 优化图表容器响应式布局 */
|
||||
@media (max-width: 1200px) {
|
||||
.visual-part>div {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#barChart,
|
||||
#pieChart {
|
||||
width: 100% !important;
|
||||
height: 350px !important;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer>>>.el-form,
|
||||
.drawer>>>.attr-list {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.drawer-body__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 18px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user