This commit is contained in:
‘937886381’
2025-12-09 13:07:09 +08:00
parent 9f3cdcb1c4
commit b9f286005c
43 changed files with 7142 additions and 729 deletions

View File

@@ -0,0 +1,65 @@
<!--
* @Author: zwq
* @Date: 2023-08-01 15:27:31
* @LastEditors: zwq
* @LastEditTime: 2023-08-01 16:25:54
* @Description:
-->
<template>
<div :class="[className, { 'p-0': noPadding }]">
<slot />
</div>
</template>
<script>
export default {
props: {
size: {
// 取值范围: xl lg md sm
type: String,
default: 'de',
validator: function (val) {
return ['xl', 'lg', 'de', 'md', 'sm'].indexOf(val) !== -1;
},
},
noPadding: {
type: Boolean,
default: false,
},
},
computed: {
className: function () {
return `${this.size}-title`;
},
},
};
</script>
<style lang="scss" scoped>
$pxls: (xl, 28px) (lg, 24px) (de, 20px) (md, 18px) (sm, 16px);
$mgr: 8px;
@each $size, $height in $pxls {
.#{$size}-title {
font-size: 18px;
line-height: $height;
color: #000;
font-weight: 500;
font-family: '微软雅黑', 'Microsoft YaHei', Arial, Helvetica, sans-serif;
&::before {
content: '';
display: inline-block;
vertical-align: top;
width: 4px;
height: $height + 2px;
border-radius: 1px;
margin-right: $mgr;
background-color: #0b58ff;
}
}
}
.p-0 {
padding: 0;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<el-dialog :visible.sync="visible" width="40%">
<small-title slot="title" :no-padding="true">
{{ this.dataForm.lineId + '·' + this.dataForm.equipmentName }}
</small-title>
<div class="content">
<div class="visual-part">
<base-table :table-props="tableProps"
:page="listQuery.pageNo" :limit="listQuery.pageSize" :table-data="tableData">
<!-- <method-btn v-if="tableBtn.length" slot="handleBtn" :width="120" label="操作" :method-list="tableBtn"
@clickBtn="handleClick" /> -->
</base-table>
</div>
</div>
<!-- <div slot="footer" class="dialog-footer">
<el-button style="" @click="goback()">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">
确定
</el-button>
</div> -->
</el-dialog>
</template>
<script>
const tableProps = [
{
prop: 'paramName',
label: '参数名称'
},
{
prop: 'paramValue',
label: '当前值',
// filter: parseTime,
// width: 160
},
];
import { getParamMonitor } from '@/api/base/equipment';
// import { parseTime } from '../../mixins/code-filter';
import SmallTitle from './SmallTitle';
export default {
components: { SmallTitle },
data() {
return {
visible: false,
tableProps,
tableData:[],
listQuery: {
pageNo: 1,
pageSize:100,
},
addOrUpdateVisible: false,
dataForm: {
equipmentId:undefined,
equipmentName: undefined,
lineId: undefined,
},
};
},
methods: {
init(data) {
console.log(data.paramMonitors,'data');
this.dataForm.equipmentId = data.equipmentId || undefined;
this.dataForm.equipmentName = data.equipmentName || undefined;
this.dataForm.lineId = data.lineId || undefined;
this.visible = true;
this.$nextTick(() => {
// this.$refs['dataForm'].resetFields();
// getParamMonitor({
// equipmentId:this.dataForm.equipmentId
// }).then((res) => {
this.tableData = data.paramMonitors
// })
});
}
},
};
</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;
/* height: 100%; */
}
.drawer >>> .visual-part {
flex: 1 auto;
max-height: 76vh;
overflow: hidden;
overflow-y: scroll;
padding-right: 10px; /* 调整滚动条样式 */
}
.drawer >>> .el-form,
.drawer >>> .attr-list {
padding: 0 16px;
}
.drawer-body__footer {
display: flex;
justify-content: flex-end;
padding: 18px;
}
</style>

View File

@@ -0,0 +1,498 @@
<template>
<el-dialog :visible.sync="visible" width="40%" @close="destroyAllCharts" 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">
<div v-show="activeLabel === 'duration'" id="barChart" style="width: 100%; height: 400px;"></div>
<!-- 报警次数-饼图 -->
<div v-show="activeLabel === 'times'" id="pieChart" style="width: 100%; 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';
// import { ElEmpty } from 'element-plus';
// 图表颜色配置(统一风格)
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: true, // 是否有数据标识
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', // 默认激活时长tab
dataForm: {
equipmentId: undefined,
equipmentName: undefined,
lineId: undefined
},
// 存储图表实例,用于销毁
chartInstances: {
bar: null,
pie: null
}
};
},
mounted() {
// 初始化默认日期(当天)
this.initDefaultDate();
},
methods: {
// 初始化默认日期当天零点到23:59:59
initDefaultDate() {
const today = new Date();
const start = new Date(today.setHours(0, 0, 0, 0)).getTime();
const end = new Date(today.setHours(23, 59, 59, 999)).getTime();
this.formConfig[0].defaultSelect = [start, end];
this.listQuery.startTime = start;
this.listQuery.endTime = end;
},
// 切换Tab时加载对应图表
handleTabClick() {
this.getDataList();
},
// 搜索按钮点击事件
buttonClick(val) {
switch (val.btnName) {
case 'search':
this.listQuery.startTime = val.timeVal?.[0];
this.listQuery.endTime = val.timeVal?.[1];
this.getDataList();
break;
case 'link':
this.disabled = true;
this.connectSSEBatch(this.listQuery.equipmentIds);
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.hasData = originData.length > 0;
if (this.hasData) {
// 根据当前Tab渲染对应图表
this.activeLabel === 'duration'
? this.renderBarChart(originData)
: this.renderPieChart(originData);
} else {
// 无数据时销毁已有图表
this.destroyAllCharts();
}
} catch (error) {
console.error('获取报警数据失败:', error);
this.hasData = false;
this.destroyAllCharts();
}
},
// 渲染报警时长-柱状图
renderBarChart(originData) {
// 先销毁原有图表
this.destroyChart('chart');
// 按报警时长降序排序
const sortedData = [...originData].sort((a, b) => b.alarmDuration - a.alarmDuration);
// 处理图表数据
const xData = sortedData.map(item => this.truncateText(item.alarmContent, 8)); // 文本截断最多8字
const seriesData = sortedData.map(item => item.alarmDuration);
const chartDom = document.getElementById('barChart');
this.chartInstances.bar = echarts.init(chartDom);
const option = {
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>报警时长:${item.alarmDuration}</div>
<div>占比:${item.alarmDurationRatio.toFixed(2)}%</div>
</div>
`;
}
},
grid: {
left: '5%',
right: '5%',
bottom: '18%', // 底部留足空间显示x轴标签
top: '10%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: xData,
axisTick: { alignWithLabel: true },
axisLabel: {
interval: 0,
// rotate: 35, // 标签旋转
fontSize: 12,
color: CHART_CONFIG.lightFontColor
},
axisLine: { lineStyle: { color: '#e8e8e8' } }
}
],
yAxis: [
{
type: 'value',
axisLabel: {
fontSize: 11,
color: CHART_CONFIG.lightFontColor,
// formatter: '{value}s' // 显示单位
},
axisLine: { lineStyle: { color: '#e8e8e8' } },
splitLine: { lineStyle: { color: '#f5f5f5' } },
// 扩展y轴最大值避免label超出
max: (value) => value.max * 1.2
}
],
series: [
{
name: '报警时长',
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);
// 监听窗口 resize
this.addResizeListener('bar');
},
// 渲染报警次数-饼图
renderPieChart(originData) {
// 先销毁原有图表
this.destroyChart('pie');
// 按报警次数降序排序
const sortedData = [...originData].sort((a, b) => b.alarmCount - a.alarmCount);
// 处理图表数据(合并占比过小的项为"其他"
const pieData = this.handlePieData(sortedData);
const chartDom = document.getElementById('pieChart');
this.chartInstances.pie = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'item',
padding: 10,
textStyle: { fontSize: 11 },
formatter: (params) => {
return `
<div style="text-align: left;">
<div>${params.name}</div>
<div>报警次数:${params.value} 次</div>
<div>占比:${params.percent.toFixed(2)}%</div>
</div>
`;
}
},
series: [
{
name: '报警次数',
type: 'pie',
radius: ['50%', '70%'], // 环形饼图
center: ['50%', '50%'], // 饼图居中显示
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); // 最多8字
return `${truncatedName}(${params.value}次, ${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,
// 高亮样式hover时
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);
// 监听窗口 resize
this.addResizeListener('pie');
},
// 处理饼图数据(合并占比<5%的项)
handlePieData(data) {
const threshold = 5; // 阈值5%
let otherCount = 0;
const mainData = data.filter(item => {
if (item.alarmCountRatio >= threshold) {
return true;
} else {
otherCount += item.alarmCount;
return false;
}
}).map(item => ({
name: item.alarmContent,
value: item.alarmCount,
ratio: item.alarmCountRatio
}));
// 如果有"其他"项,添加到数据中
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;
},
// 添加窗口resize监听
addResizeListener(type) {
const chart = this.chartInstances[type];
if (chart) {
const resizeHandler = () => chart.resize();
window.addEventListener('resize', resizeHandler);
// 存储resize处理器用于后续移除
chart.resizeHandler = resizeHandler;
}
},
// 销毁单个图表
destroyChart(type) {
const chart = this.chartInstances[type];
if (chart) {
// 移除resize监听
window.removeEventListener('resize', chart.resizeHandler);
chart.dispose();
this.chartInstances[type] = null;
}
},
// 销毁所有图表
destroyAllCharts() {
Object.keys(this.chartInstances).forEach(type => {
this.destroyChart(type);
});
},
// 初始化弹窗数据
init(data) {
this.dataForm = {
equipmentId: data.equipmentId || undefined,
equipmentName: data.equipmentName || '未知设备',
lineId: data.lineId || '未知产线'
};
this.listQuery.equipmentId = data.equipmentId || undefined;
this.visible = true;
this.$nextTick(() => {
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;
/* height: 100%; */
}
.drawer>>>.visual-part {
flex: 1 auto;
max-height: 76vh;
overflow: hidden;
overflow-y: scroll;
padding-right: 10px;
/* 调整滚动条样式 */
}
.drawer>>>.el-form,
.drawer>>>.attr-list {
padding: 0 16px;
}
.drawer-body__footer {
display: flex;
justify-content: flex-end;
padding: 18px;
}
</style>

File diff suppressed because it is too large Load Diff