1309 lines
39 KiB
Vue
1309 lines
39 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 1. 顶部导航栏(固定高度) -->
|
||
<!-- <div class="top-nav">
|
||
</div> -->
|
||
|
||
<!-- 2. 搜索栏(固定高度,占自身空间) -->
|
||
<div class="search-container energyOverlimitLog">
|
||
<span class="blue-block"></span>
|
||
<span class="tip">设备总览</span>
|
||
<search-bar removeBlue :formConfigs="formConfig" ref="searchBarForm" @headBtnClick="buttonClick" />
|
||
</div>
|
||
<div class="num-container">
|
||
<div class="equipmentNum">
|
||
<div class="equipment-info">
|
||
<div class="info">
|
||
<div class="num equipment">
|
||
{{ globalCount.total ? globalCount.total : 0 }}
|
||
</div>
|
||
<div class="title">
|
||
设备数量
|
||
</div>
|
||
</div>
|
||
<img class="numImg" style="width: 86px;height: 86px;" src="../../../assets/images/equipmentNumImg.png" alt="">
|
||
</div>
|
||
</div>
|
||
<div class="runNum">
|
||
<div class="equipment-info">
|
||
<div class="info">
|
||
<div class="num run">
|
||
{{ globalCount.run ? globalCount.run : 0 }}
|
||
</div>
|
||
<div class="title">
|
||
运行数量
|
||
</div>
|
||
</div>
|
||
<img class="numImg" style="width: 86px;height: 86px;" src="../../../assets/images/runNumImg.png" alt="">
|
||
</div>
|
||
</div>
|
||
<div class="stopNum">
|
||
<div class="equipment-info">
|
||
<div class="info">
|
||
<div class="num stop">
|
||
{{ globalCount.stop ? globalCount.stop : 0 }}
|
||
</div>
|
||
<div class="title">
|
||
停机数量
|
||
</div>
|
||
</div>
|
||
<img class="numImg" style="width: 86px;height: 86px;" src="../../../assets/images/stopNumImg.png" alt="">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="content-container energyOverlimitLog">
|
||
<span class="blue-block"></span>
|
||
<span class="tip">设备运行情况</span>
|
||
<div class="content">
|
||
<el-tabs class="custom-tabs" v-model="activeLabel" :stretch="true" @tab-click="handleTabClick">
|
||
<el-tab-pane :label="'全部'" name="all">
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="'\u3000运行中\u3000'" name="running">
|
||
</el-tab-pane>
|
||
<el-tab-pane :label="'\u3000停机\u3000'" name="stop">
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="circle run"></div>
|
||
<div class="title">运行中</div>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="circle stop"></div>
|
||
<div class="title">停机</div>
|
||
</div>
|
||
</div>
|
||
<div class="eqRun">
|
||
<!-- 循环渲染设备列表:key绑定唯一equipmentId -->
|
||
<div v-for="item in filteredList" :key="item.equipmentId" class="eqItem" :class="{
|
||
'eq-item-disabled-false': !disabled,
|
||
'eq-item-disabled-true': disabled
|
||
}">
|
||
<div class="title">
|
||
<div class="eqName">
|
||
<!-- 设备状态图标:status=0→运行中,其他→停机(根据实际业务调整) -->
|
||
<div class="circle" :class="{ run: item.status === 0, stop: item.status !== 0 }"
|
||
:style="!disabled ? { background: '#B4B4B4', boxShadow: '0px 0px 6px 0px #B4B4B4' } : {}"></div>
|
||
<div class="name" :style="!disabled ? { color: 'rgba(180, 180, 180, 1)' } : {}">
|
||
<!-- 显示产线+设备名称(根据list数据调整字段) -->
|
||
产线 {{ item.lineId }} · {{ item.equipmentName }}
|
||
</div>
|
||
</div>
|
||
<!-- 警告数量(这里假设用模拟数据,实际需从接口获取) -->
|
||
<el-tooltip v-if="disabled && item.equipmentAlarms" placement="bottom" effect="light"
|
||
popper-class="no-border-tooltip" :visible-arrow="false">
|
||
<div slot="content">
|
||
<div class="tooltips-item" v-for="(alarm, index) in item.equipmentAlarms" :key="index">
|
||
<div class="line" />
|
||
<div class="text">
|
||
<div class="time">
|
||
{{ formatTime(alarm.time) }}
|
||
</div>
|
||
<div class="alarm">
|
||
{{ alarm.content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="alarmNum">
|
||
{{ item.equipmentCount }} 条警告
|
||
</div>
|
||
</el-tooltip>
|
||
</div>
|
||
|
||
<div class="warningHours">
|
||
<div class="hours" style="font-size: 14px;" :style="!disabled ? { color: 'rgba(180, 180, 180, 1)' } : {}">
|
||
24 小时运行状态
|
||
</div>
|
||
<!-- 参数异常提示(模拟数据) -->
|
||
<el-tooltip v-if="disabled && item.paramAlarms" placement="bottom" effect="light"
|
||
popper-class="no-border-tooltip" :visible-arrow="false">
|
||
<div slot="content">
|
||
<div class="tooltips-item" v-for="(alarm, index) in item.paramAlarms" :key="index">
|
||
<div class="line" />
|
||
<div class="text">
|
||
<div class="time">
|
||
{{ formatTime(alarm.time) }}
|
||
</div>
|
||
<div class="alarm">
|
||
参数名称 {{ alarm.paramName }} 报警值 {{ alarm.paramValue }},
|
||
<!-- 超出上限 -->
|
||
<span v-if="alarm.overMax">
|
||
超出上限值
|
||
<span style="color: rgba(255, 189, 2, 1);">
|
||
{{
|
||
((Number(alarm.paramValue) || 0) - (Number(alarm.maxValue) || 0))
|
||
.toFixed(2)
|
||
.replace(/\.?0*$/, '')
|
||
}}
|
||
</span>
|
||
</span>
|
||
<!-- 超出下限 -->
|
||
<span v-else-if="alarm.overMin">
|
||
超出下限值
|
||
<span style="color: rgba(255, 189, 2, 1);">
|
||
{{
|
||
((Number(alarm.minValue) || 0) - (Number(alarm.paramValue) || 0))
|
||
.toFixed(2)
|
||
.replace(/\.?0*$/, '')
|
||
}}
|
||
</span>
|
||
</span>
|
||
<!-- <span v-else>无超出阈值</span> -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="warning">
|
||
{{ item.paramCount }} 条参数异常
|
||
</div>
|
||
</el-tooltip>
|
||
</div>
|
||
|
||
<div class="progress-container">
|
||
<!-- 运行进度条:绑定item.run的百分比 -->
|
||
<div class="progress-running" :style="!disabled ? { width: '0%' } : { width: `${item.run}%` }"></div>
|
||
</div>
|
||
|
||
<div class="progress-text">
|
||
<!-- 运行百分比:显示item.run的值(增加值存在性判断) -->
|
||
<div class="run" :style="!disabled ? { color: 'rgba(180, 180, 180, 1)' } : {}">
|
||
运行 {{
|
||
!disabled
|
||
? '—'
|
||
: (item.run !== undefined && item.run !== null && !isNaN(item.run))
|
||
? `${item.run.toFixed(1) }%`
|
||
: '—'
|
||
}}
|
||
</div>
|
||
<!-- 停机百分比:显示item.stop的值(增加值存在性判断) -->
|
||
<div class="stop" :style="!disabled ? { color: 'rgba(180, 180, 180, 1)' } : {}">
|
||
停机 {{
|
||
!disabled
|
||
? '—'
|
||
: (item.stop !== undefined && item.stop !== null && !isNaN(item.stop))
|
||
? `${item.stop.toFixed(1) }%`
|
||
: '—'
|
||
}}
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="disabled" class="bottom">
|
||
<div @click="handleGetData(item)" class="runBottom">数据监控</div>
|
||
<div class="runBottom" @click="handleAlarm(item)">报警统计</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 无数据提示 -->
|
||
<!-- <div v-if="filteredList.length === 0" class="no-data">
|
||
暂无设备运行数据
|
||
</div> -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList" />
|
||
<alarm-or-update v-if="alarmOrUpdateVisible" ref="alarmOrUpdate" @refreshDataList="getDataList" />
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
// import { parseTime } from '@/filter/code-filter';
|
||
import {
|
||
getPdList,
|
||
} from '@/api/core/monitoring/auto';
|
||
// import { getFactoryPage } from '@/api/core/base/factory';
|
||
import { getTree } from '@/api/base/equipment';
|
||
import { getEquipmentTypePage } from '@/api/base/equipmentType';
|
||
import { getEquipmentOverall } from '@/api/base/equipment';
|
||
|
||
// import * as XLSX from 'xlsx';
|
||
// import FileSaver from 'file-saver';
|
||
import ButtonNav from '@/components/ButtonNav';
|
||
// import { formatTime } from '@/utils';
|
||
import { getAccessToken } from '@/utils/auth';
|
||
|
||
import AddOrUpdate from './add-or-updata';
|
||
import alarmOrUpdate from './alarm-or-updata';
|
||
|
||
import store from "@/store";
|
||
export default {
|
||
components: { ButtonNav, AddOrUpdate, alarmOrUpdate },
|
||
data() {
|
||
return {
|
||
urlOptions: { getDataListURL: getEquipmentOverall },
|
||
disabled: false,
|
||
listQuery: {
|
||
equipmentIds: undefined,
|
||
lineId: undefined,
|
||
equipmentTypeId: undefined,
|
||
status: undefined,
|
||
},
|
||
status: undefined,
|
||
countEq: undefined,
|
||
countRun: undefined,
|
||
countStop: undefined,
|
||
addOrUpdateVisible: false,
|
||
activeLabel: 'all',
|
||
list: [], // 折线图数据
|
||
formConfig: [
|
||
|
||
{
|
||
type: 'select',
|
||
label: '产线',
|
||
selectOptions: [],
|
||
param: 'lineId',
|
||
onchange: true
|
||
},
|
||
{
|
||
type: 'select',
|
||
label: '设备类型',
|
||
selectOptions: [],
|
||
param: 'equipmentTypeId',
|
||
// labelField: 'label',
|
||
// valueField: 'label',
|
||
},
|
||
{
|
||
type: 'cascader',
|
||
label: '设备名称',
|
||
selectOptions: [],
|
||
param: 'equipmentIds',
|
||
showAllLevels: false,
|
||
clearable: false,
|
||
cascaderProps: {
|
||
multiple: true,
|
||
filterable: true,
|
||
emitPath: false,
|
||
label: 'name', // 提前配置,后续可覆盖
|
||
value: 'id'
|
||
},
|
||
collapseTags: true,
|
||
width: 250,
|
||
// type: 'select',
|
||
// label: '设备选择',
|
||
// selectOptions: [],
|
||
// param: 'equipmentId',
|
||
labelField: 'name',
|
||
valueField: 'id',
|
||
},
|
||
{ type: 'button', btnName: '查询', name: 'search', color: 'primary' },
|
||
{ type: 'separate' },
|
||
{
|
||
type: 'button', btnName: '连接', name: 'link', plain: true,
|
||
color: 'primary',
|
||
},
|
||
// {
|
||
// type: 'button',
|
||
// btnName: '重置',
|
||
// name: 'reset',
|
||
// },
|
||
],
|
||
searchBarData:{},
|
||
alarmOrUpdateVisible:false,
|
||
sseReader: null, // 保存流读取器
|
||
abortController: null, // 用于中止 fetch 请求
|
||
retryCount: 0, // 当前重试次数
|
||
isDestroyed: false, // 标记组件是否已销毁
|
||
isSSEConnected: false, // 标记是否已建立 SSE 连接(避免重复连接)
|
||
currentSSEReaders: [], // 保存所有设备的 SSE 读取器(用于批量关闭)
|
||
};
|
||
},
|
||
mounted() {
|
||
this.getLineData()
|
||
this.getDataList()
|
||
},
|
||
computed: {
|
||
// 筛选后的列表(按 status 变化,不影响统计)
|
||
filteredList() {
|
||
if (this.status === undefined) {
|
||
return this.list; // 全部数据
|
||
}
|
||
// 只筛选列表,不改变统计
|
||
return this.list.filter(item => item.status === this.status);
|
||
},
|
||
// 全局统计(基于原始 list,不随 status 变化)
|
||
globalCount() {
|
||
// 优先使用接口返回的统计数据(如果接口提供)
|
||
if (this.countEq > 0 || this.countRun > 0 || this.countStop > 0) {
|
||
return {
|
||
total: this.countEq,
|
||
run: this.countRun,
|
||
stop: this.countStop
|
||
};
|
||
}
|
||
// 接口未提供时,基于原始 list 计算(兜底方案)
|
||
return {
|
||
total: this.list.length,
|
||
run: this.list.filter(item => item.status === 0).length,
|
||
stop: this.list.filter(item => item.status === 1).length
|
||
};
|
||
}
|
||
},
|
||
methods: {
|
||
formatTime(time) {
|
||
// 处理时间戳:10位秒级转13位毫秒级
|
||
let timestamp = typeof time === 'number' ? time : Date.parse(time);
|
||
if (timestamp.toString().length === 10) {
|
||
timestamp *= 1000;
|
||
}
|
||
|
||
const date = new Date(timestamp);
|
||
// 补零函数:确保数字为两位数
|
||
const padZero = (num) => num.toString().padStart(2, '0');
|
||
|
||
// 提取时间分量
|
||
const year = padZero(date.getFullYear().toString().slice(-2)); // 取年份后两位(如 2025 → 25)
|
||
const month = padZero(date.getMonth() + 1); // 月份从0开始,+1后补零
|
||
const day = padZero(date.getDate());
|
||
const hour = padZero(date.getHours());
|
||
const minute = padZero(date.getMinutes());
|
||
const second = padZero(date.getSeconds());
|
||
|
||
// 拼接为目标格式:时分秒/年.月.日
|
||
return `${hour}:${minute}:${second}/${year}.${month}.${day}`;
|
||
},
|
||
/**
|
||
* 接收数组格式的树形数据,去掉最后一级的 children 属性
|
||
* @param {Array} treeArray - 树形结构数组(即原 response.data)
|
||
* @returns {Array} 处理后的树形数组(结构不变,仅删最后一级 children)
|
||
*/
|
||
transformAndRemoveLastChildren(treeArray) {
|
||
// 深拷贝:避免修改原数组数据
|
||
const deepClone = (obj) => {
|
||
if (obj === null || typeof obj !== 'object') return obj;
|
||
if (Array.isArray(obj)) {
|
||
const arrClone = [];
|
||
for (let i = 0; i < obj.length; i++) {
|
||
arrClone[i] = deepClone(obj[i]);
|
||
}
|
||
return arrClone;
|
||
}
|
||
const objClone = {};
|
||
for (const key in obj) {
|
||
if (obj.hasOwnProperty(key)) {
|
||
objClone[key] = deepClone(obj[key]);
|
||
}
|
||
}
|
||
return objClone;
|
||
};
|
||
|
||
// 深拷贝传入的数组
|
||
const clonedArray = deepClone(treeArray) || [];
|
||
|
||
// 递归处理节点:只删最后一级 children
|
||
const processNode = (node) => {
|
||
if (!node || typeof node !== 'object') return node;
|
||
|
||
// 数组直接递归处理每个子项
|
||
if (Array.isArray(node)) {
|
||
return node.map(item => processNode(item));
|
||
}
|
||
|
||
const processedNode = { ...node };
|
||
|
||
// 判断是否为最后一级
|
||
let isLastLevel = true;
|
||
if (Array.isArray(processedNode.children) && processedNode.children.length > 0) {
|
||
isLastLevel = !processedNode.children.some(child =>
|
||
Array.isArray(child.children) && child.children.length > 0
|
||
);
|
||
}
|
||
|
||
// 最后一级删除 children
|
||
if (isLastLevel) {
|
||
delete processedNode.children;
|
||
} else {
|
||
processedNode.children = processNode(processedNode.children);
|
||
}
|
||
|
||
return processedNode;
|
||
};
|
||
|
||
// 处理数组中的所有节点
|
||
return processNode(clonedArray);
|
||
},
|
||
async getLineData() {
|
||
getPdList().then(res => {
|
||
this.formConfig[0].selectOptions = res.data || [];
|
||
})
|
||
getEquipmentTypePage({
|
||
pageSize: 100,
|
||
pageNo: 1
|
||
}).then(res => {
|
||
this.formConfig[1].selectOptions = res.data.list || [];
|
||
})
|
||
// getEquipmentPage({
|
||
// pageSize: 100,
|
||
// pageNo: 1
|
||
// }).then(res => {
|
||
// this.formConfig[2].selectOptions = res.data.list || [];
|
||
// })
|
||
const { data } = await this.$axios('/base/factory/getTree');
|
||
this.formConfig[2].selectOptions = data
|
||
// console.log('this.removeLastLevelChildren(data)', this.transformAndRemoveLastChildren(data))
|
||
},
|
||
|
||
// 切换按产线/按产品监控
|
||
|
||
// 搜索/导出按钮点击
|
||
buttonClick(val) {
|
||
switch (val.btnName) {
|
||
case 'search':
|
||
this.listQuery.pageNo = 1;
|
||
this.listQuery.pageSize = 10;
|
||
this.searchBarData = val
|
||
// console.log('val.equipmentIds', val.equipmentIds);
|
||
this.getDataList();
|
||
// 关键:触发 SSE 连接(先断开旧连接,再循环连接新设备)
|
||
break;
|
||
case 'link':
|
||
this.disabled = true;
|
||
let equipmentIds = []
|
||
let arr = []
|
||
this.list.forEach(ele => {
|
||
console.log('ele',ele);
|
||
|
||
arr.push(ele.equipmentId)
|
||
});
|
||
console.log('arr', arr);
|
||
|
||
equipmentIds = arr
|
||
this.connectSSEBatch(this.listQuery.equipmentIds.length > 0 ? this.listQuery.equipmentIds : equipmentIds);
|
||
break;
|
||
default:
|
||
}
|
||
},
|
||
|
||
getDataList() {
|
||
if (this.disabled === true) {
|
||
this.closeAllSSE(); // 直接调用批量关闭方法
|
||
this.disabled = false;
|
||
this.isSSEConnected = false; // 重置连接状态
|
||
}
|
||
this.dataListLoading = true;
|
||
this.listQuery.equipmentIds = this.searchBarData.equipmentIds || [];
|
||
// this.listQuery.equipmentIds = [200301, 200302, 200303];
|
||
|
||
this.listQuery.lineId = this.searchBarData.lineId ? this.searchBarData.lineId : undefined;
|
||
this.listQuery.equipmentTypeId = this.searchBarData.equipmentTypeId ? this.searchBarData.equipmentTypeId : undefined
|
||
// this.listQuery.status = this.status ? this.status : undefined
|
||
this.urlOptions.getDataListURL(this.listQuery).then(res => {
|
||
this.list = res.data.dets
|
||
this.countEq = res.data.countEq
|
||
this.countRun = res.data.countRun
|
||
this.countStop = res.data.countStop
|
||
// console.log(this.tableDataCustom);
|
||
});
|
||
},
|
||
handleGetData(data) {
|
||
this.addOrUpdateVisible = true
|
||
this.$nextTick(() => {
|
||
this.$refs.addOrUpdate.init(data);
|
||
});
|
||
},
|
||
handleAlarm(data) {
|
||
this.alarmOrUpdateVisible = true
|
||
this.$nextTick(() => {
|
||
this.$refs.alarmOrUpdate.init(data);
|
||
});
|
||
},
|
||
handleTabClick() {
|
||
console.log('activeLabel', this.activeLabel);
|
||
if (this.activeLabel === 'running') {
|
||
this.status = 0
|
||
} else if (this.activeLabel === 'stop') {
|
||
this.status = 1
|
||
} else{
|
||
this.status = undefined
|
||
}
|
||
},
|
||
closeSSE() {
|
||
this.isDestroyed = true;
|
||
this.closeAllSSE(); // 调用批量关闭方法
|
||
console.log('组件销毁:SSE 所有连接已强制关闭');
|
||
},
|
||
isValidData(data) {
|
||
return data.trim().startsWith('data:{') && !data.includes('heartbeat');
|
||
},
|
||
// 方案1:箭头函数绑定 this(推荐)
|
||
connectSSEBatch(equipmentIds) {
|
||
console.log('equipmentIds'.equipmentIds);
|
||
|
||
this.closeAllSSE(); // 此时 this 指向组件实例
|
||
|
||
if (!equipmentIds || equipmentIds.length === 0) {
|
||
console.log('未选中任何设备,无需建立 SSE 连接');
|
||
return;
|
||
}
|
||
|
||
// 循环调用 getData,this 正确指向组件
|
||
for (const equipmentId of equipmentIds) {
|
||
this.getData(equipmentId);
|
||
}
|
||
|
||
this.isSSEConnected = true;
|
||
},
|
||
|
||
// 批量关闭所有 SSE 连接
|
||
closeAllSSE() {
|
||
this.isSSEConnected = false;
|
||
if (this.abortController) {
|
||
this.abortController.abort();
|
||
this.abortController = null;
|
||
}
|
||
this.currentSSEReaders.forEach(reader => {
|
||
reader.cancel().catch(err => console.log('关闭 SSE 读取器失败:', err));
|
||
});
|
||
this.currentSSEReaders = [];
|
||
console.log('所有 SSE 连接已断开');
|
||
},
|
||
|
||
// 异步连接单个设备的 SSE
|
||
async getData(equipmentId) {
|
||
console.log('开始连接设备 SSE:', equipmentId);
|
||
if (this.isDestroyed) return;
|
||
|
||
const url = process.env.VUE_APP_BASE_API +
|
||
`/admin-api/monitoring/equMonitorMessage/subscribe/${equipmentId}-${Date.now()}`;
|
||
|
||
const token = getAccessToken();
|
||
const headers = new Headers({
|
||
Authorization: `Bearer ${token}`,
|
||
'tenant-id': store.getters.userId,
|
||
'Content-Type': 'text/event-stream',
|
||
});
|
||
|
||
try {
|
||
if (!this.abortController) {
|
||
this.abortController = new AbortController();
|
||
}
|
||
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: headers,
|
||
signal: this.abortController.signal,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`设备 ${equipmentId} SSE 连接失败:${response.status} - ${response.statusText}`);
|
||
return;
|
||
}
|
||
|
||
const sseReader = response.body.getReader();
|
||
this.currentSSEReaders.push(sseReader);
|
||
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await sseReader.read();
|
||
if (done) {
|
||
console.log(`设备 ${equipmentId} SSE 连接正常关闭`);
|
||
this.currentSSEReaders = this.currentSSEReaders.filter(reader => reader !== sseReader);
|
||
if (!this.isDestroyed) {
|
||
this.handleReconnect(equipmentId);
|
||
}
|
||
break;
|
||
}
|
||
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
buffer += chunk;
|
||
const messages = buffer.split('\n\n');
|
||
buffer = messages.pop() || '';
|
||
|
||
for (const message of messages) {
|
||
if (this.isValidData(message)) {
|
||
this.upDateMsg(message, equipmentId);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (error.name === 'AbortError') return;
|
||
console.error(`设备 ${equipmentId} SSE 连接异常:`, error);
|
||
if (!this.isDestroyed) {
|
||
this.handleReconnect(equipmentId);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 处理 SSE 推送数据
|
||
upDateMsg(data, equipmentId) {
|
||
const jsonStr = data.replace(/^data:/, '').trim();
|
||
console.log(`设备 ${equipmentId} 收到 SSE 数据:`, jsonStr);
|
||
|
||
try {
|
||
const dataObj = JSON.parse(jsonStr);
|
||
this.dataObj = dataObj;
|
||
|
||
// 更新对应设备的报警数量
|
||
const targetEqIndex = this.list.findIndex(item => item.equipmentId === equipmentId);
|
||
if (targetEqIndex !== -1) {
|
||
const updatedEq = { ...this.list[targetEqIndex] };
|
||
updatedEq.paramAlarms = dataObj.paramAlarms ? dataObj.paramAlarms : [];
|
||
updatedEq.paramCount = dataObj.paramAlarms ? dataObj.paramAlarms.length : 0;
|
||
// 更新参数异常数量(equipmentAlarms 长度)
|
||
updatedEq.equipmentAlarms = dataObj.equipmentAlarms ? dataObj.equipmentAlarms : [];
|
||
updatedEq.equipmentCount = dataObj.equipmentAlarms ? dataObj.equipmentAlarms.length : 0;
|
||
updatedEq.paramMonitors = dataObj.paramMonitors ? dataObj.paramMonitors : [];
|
||
this.list.splice(targetEqIndex, 1, updatedEq);
|
||
}
|
||
console.log(' this.list', this.list);
|
||
|
||
|
||
// if (this.list.length > 0) { // 确保数组有数据
|
||
// const firstEqIndex = 0; // 固定取第一个元素
|
||
// const updatedEq = { ...this.list[firstEqIndex] }; // 深拷贝第一个元素
|
||
|
||
// // 更新警告数量(paramAlarms 长度)
|
||
// updatedEq.paramAlarms = dataObj.paramAlarms ? dataObj.paramAlarms : [];
|
||
// updatedEq.paramCount = dataObj.paramAlarms ? dataObj.paramAlarms.length : 0;
|
||
// // 更新参数异常数量(equipmentAlarms 长度)
|
||
// updatedEq.equipmentAlarms = dataObj.equipmentAlarms ? dataObj.equipmentAlarms : [];
|
||
// updatedEq.equipmentCount = dataObj.equipmentAlarms ? dataObj.equipmentAlarms.length : 0;
|
||
// updatedEq.paramMonitors = dataObj.paramMonitors ? dataObj.paramMonitors : [];
|
||
// // 响应式更新数组第一个元素(Vue 会检测到变化并刷新页面)
|
||
// this.list.splice(firstEqIndex, 1, updatedEq);
|
||
// }
|
||
|
||
// console.log('更新后的 list 数组:', this.list);
|
||
} catch (e) {
|
||
console.error(`设备 ${equipmentId} SSE 数据解析失败:`, e);
|
||
}
|
||
},
|
||
|
||
// 重连方法
|
||
handleReconnect(equipmentId) {
|
||
if (this.isDestroyed) return;
|
||
const maxRetries = 5;
|
||
if (this.retryCount < maxRetries) {
|
||
const delay = Math.pow(2, this.retryCount) * 1000;
|
||
setTimeout(() => {
|
||
this.retryCount++;
|
||
this.getData(equipmentId);
|
||
}, delay);
|
||
} else {
|
||
console.error(`设备 ${equipmentId} SSE 重连次数已达上限`);
|
||
this.retryCount = 0;
|
||
}
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// 全局容器:占满屏幕高度,减去90px(可根据实际需求调整)
|
||
.app-container {
|
||
width: 100%;
|
||
height: calc(100vh - 90px);
|
||
background: #f2f4f9;
|
||
padding: 8px 0px;
|
||
// display: flex;
|
||
// flex-direction: column;
|
||
// overflow: hidden;
|
||
}
|
||
|
||
// 顶部导航栏:固定高度40px
|
||
.top-nav {
|
||
height: 40px;
|
||
width: 100%;
|
||
background: #f2f4f9;
|
||
}
|
||
|
||
// 搜索栏:固定高度(自身内容高度),底部间距8px
|
||
.search-container {
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
border-radius: 8px;
|
||
padding: 16px 16px 18px 16px;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.content-container {
|
||
// width: 1640px;
|
||
// height: 730px;
|
||
// background: #FFFFFF;
|
||
border-radius: 8px;
|
||
margin-top: 8px;
|
||
height: calc(100vh - 350px);
|
||
padding: 16px 16px 18px 16px;
|
||
background-color: #ffffff;
|
||
|
||
.content {
|
||
position: relative;
|
||
height: calc(100% - 40px); // 减去标签栏/图例的占用高度(40px 可微调)
|
||
overflow: hidden; // 隐藏父容器溢出,避免影响整体布局
|
||
// display: flex;
|
||
|
||
.legend {
|
||
display: flex;
|
||
position: absolute;
|
||
margin-left: 24px;
|
||
gap: 14px;
|
||
top: -3px;
|
||
left: 400px;
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
|
||
.circle {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.run {
|
||
background: #0BDBFF;
|
||
box-shadow: 0px 0px 6px 0px #0BDBFF;
|
||
}
|
||
|
||
.stop {
|
||
background: #FF760B;
|
||
box-shadow: 0px 0px 6px 0px #FF760B;
|
||
}
|
||
|
||
.title {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
// line-height: 16px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
}
|
||
|
||
.eqRun {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap; // 保留换行功能
|
||
height: 100%; // 继承父容器高度,作为滚动触发条件
|
||
overflow-y: auto; // 超出高度时显示滚动功能(可手动滑动)
|
||
overflow-x: hidden; // 禁止横向滚动,避免布局错乱
|
||
padding: 0px 0 16px 0; // 上下预留空间,避免最后一行被遮挡
|
||
|
||
// 隐藏滚动条(兼容主流浏览器,视觉更简洁)
|
||
&::-webkit-scrollbar {
|
||
width: 0; // Chrome/Safari 滚动条宽度设为0
|
||
height: 0;
|
||
}
|
||
|
||
scrollbar-width: none; // Firefox 隐藏滚动条
|
||
-ms-overflow-style: none; // IE/Edge 隐藏滚动条
|
||
|
||
.eq-item-disabled-false {
|
||
width: 394px !important;
|
||
height: 142px !important;
|
||
background: #FFFFFF !important;
|
||
border-radius: 8px !important;
|
||
border: 1px solid #E0E0E0 !important;
|
||
|
||
// 取消 hover 效果(disabled=false 时不需要高亮)
|
||
// &:hover {
|
||
// box-shadow: none !important;
|
||
|
||
// .bottom {
|
||
// .runBottom {
|
||
// background: #E6F1FC !important;
|
||
// box-shadow: none !important;
|
||
// opacity: 0.4 !important;
|
||
// }
|
||
// }
|
||
// }
|
||
}
|
||
|
||
// 2. disabled 为 true 时的样式(保留原有样式,统一放在此类中)
|
||
.eq-item-disabled-true {
|
||
width: 394px;
|
||
height: 142px;
|
||
background: #FFFFFF;
|
||
border-radius: 8px;
|
||
border: 1px solid #C2D5FF;
|
||
|
||
&:hover {
|
||
box-shadow: 0px 2px 6px 0px #D7D7D7;
|
||
|
||
.bottom {
|
||
.runBottom {
|
||
background: #E6F1FC !important;
|
||
box-shadow: 0px 2px 6px 0px #D7D7D7 !important;
|
||
border-radius: 4px !important;
|
||
border: 1px solid #A3D0FD !important;
|
||
opacity: 1 !important;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.eqItem {
|
||
// width: 394px;
|
||
// height: 142px;
|
||
// background: #FFFFFF;
|
||
// border-radius: 8px;
|
||
// border: 1px solid #C2D5FF;
|
||
|
||
// &:hover {
|
||
// box-shadow: 0px 2px 6px 0px #D7D7D7;
|
||
|
||
// // opacity: .1 !important; // 取消原有透明效果,让样式更清晰
|
||
// .bottom {
|
||
// .runBottom {
|
||
// background: #E6F1FC !important; // 覆盖原有背景
|
||
// box-shadow: 0px 2px 6px 0px #D7D7D7 !important; // 添加阴影
|
||
// border-radius: 4px !important;
|
||
// border: 1px solid #A3D0FD !important;
|
||
// opacity: 1 !important; // 取消原有透明效果,让样式更清晰
|
||
// cursor: pointer; // 可选:添加指针样式,提示可点击
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
.title {
|
||
display: flex;
|
||
padding: 14px 22px 0 16px;
|
||
justify-content: space-between;
|
||
|
||
.eqName {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
|
||
.circle {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.run {
|
||
background: #0BDBFF;
|
||
box-shadow: 0px 0px 6px 0px #0BDBFF;
|
||
}
|
||
|
||
.stop {
|
||
background: #FF760B;
|
||
box-shadow: 0px 0px 6px 0px #FF760B;
|
||
}
|
||
|
||
.name {
|
||
// width: 133px;
|
||
// height: 16px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
}
|
||
|
||
.alarmNum {
|
||
width: 67px;
|
||
height: 18px;
|
||
margin-bottom: -2px;
|
||
background: #FFEFEC;
|
||
// box-shadow: 0px 2px 6px 0px #D7D7D7;
|
||
border-radius: 3px;
|
||
border: 1px solid #FF5454;
|
||
// width: 58px;
|
||
// height: 16px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #FF2020;
|
||
line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: center;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.warningHours {
|
||
display: flex;
|
||
padding: 9px 22px 0 16px;
|
||
justify-content: space-between;
|
||
|
||
.hours {
|
||
// width: 133px;
|
||
// height: 16px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
// line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.warning {
|
||
margin-bottom: -2px;
|
||
width: 94px;
|
||
height: 18px;
|
||
background: #FFF7DF;
|
||
// box-shadow: 0px 2px 6px 0px #D7D7D7;
|
||
border-radius: 3px;
|
||
border: 1px solid #FFBD02;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #FF760B;
|
||
line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: center;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.progress-container {
|
||
margin: 8px 24px 0 16px;
|
||
width: 348px;
|
||
height: 14px;
|
||
background: #F1F1F1;
|
||
// box-shadow: 0px 2px 6px 0px #D7D7D7;
|
||
border-radius: 9px;
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
color: #333;
|
||
position: relative;
|
||
}
|
||
|
||
.progress-running {
|
||
// width: 70%;
|
||
height: 100%;
|
||
background: #1677ff;
|
||
float: left;
|
||
line-height: 24px;
|
||
// padding-left: 8px;
|
||
}
|
||
|
||
.progress-text {
|
||
margin: 4px 24px 0 24px;
|
||
width: 348px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 4px;
|
||
|
||
.run {
|
||
// width: 133px;
|
||
// height: 16px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #0B58FF;
|
||
line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.stop {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #7D7D7D;
|
||
line-height: 16px;
|
||
text-shadow: 0px 2px 6px #D7D7D7;
|
||
text-align: right;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.bottom {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
|
||
.runBottom {
|
||
width: 198px;
|
||
height: 32px;
|
||
background: #E6F1FC;
|
||
border-radius: 4px;
|
||
border: 1px solid #A3D0FD;
|
||
opacity: 0.4;
|
||
// width: 120px;
|
||
// height: 20px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #0B58FF;
|
||
line-height: 30px;
|
||
text-align: center;
|
||
font-style: normal;
|
||
text-transform: uppercase;
|
||
letter-spacing: 2px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.num-container {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
gap: 8px;
|
||
display: flex;
|
||
|
||
.equipmentNum {
|
||
background-image: url('../../../assets/images/equipmentNum.png');
|
||
background-size: 100% 100%;
|
||
width: 541px;
|
||
height: 104px;
|
||
|
||
.equipment-info {
|
||
margin-left: 110px;
|
||
display: flex;
|
||
|
||
.info {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.num {
|
||
width: 143px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 600;
|
||
font-size: 33px;
|
||
line-height: 28px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.equipment {
|
||
color: #0B58FF;
|
||
}
|
||
|
||
.title {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
line-height: 19px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.numImg {
|
||
margin-top: 11px;
|
||
margin-left: 99px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.runNum {
|
||
background-image: url('../../../assets/images/runNum.png');
|
||
background-size: 100% 100%;
|
||
width: 541px;
|
||
height: 104px;
|
||
|
||
.equipment-info {
|
||
margin-left: 110px;
|
||
display: flex;
|
||
|
||
.info {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.num {
|
||
width: 143px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 600;
|
||
font-size: 33px;
|
||
color: #0B58FF;
|
||
line-height: 28px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.run {
|
||
color: rgba(76, 208, 231, 1);
|
||
}
|
||
|
||
.title {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
line-height: 19px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.numImg {
|
||
margin-top: 11px;
|
||
margin-left: 99px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.stopNum {
|
||
background-image: url('../../../assets/images/stopNum.png');
|
||
background-size: 100% 100%;
|
||
width: 541px;
|
||
height: 104px;
|
||
|
||
.equipment-info {
|
||
margin-left: 110px;
|
||
display: flex;
|
||
|
||
.info {
|
||
margin-top: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.num {
|
||
width: 143px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 600;
|
||
font-size: 33px;
|
||
color: #0B58FF;
|
||
line-height: 28px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.stop {
|
||
color: rgba(255, 118, 11, 1);
|
||
}
|
||
|
||
.title {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
color: #000000;
|
||
line-height: 19px;
|
||
letter-spacing: 2px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.numImg {
|
||
margin-top: 11px;
|
||
margin-left: 99px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.custom-tabs) {
|
||
.el-tabs__header {
|
||
margin-bottom: 8px;
|
||
display: inline-block;
|
||
transform: translateY(-12px);
|
||
}
|
||
|
||
.el-tabs__content {
|
||
overflow: visible;
|
||
}
|
||
|
||
.el-tabs__item {
|
||
padding-left: 0 !important;
|
||
padding-right: 0 !important;
|
||
line-height: 36px !important;
|
||
width: 132px;
|
||
letter-spacing: 2px;
|
||
height: 36px;
|
||
}
|
||
}
|
||
|
||
.tooltips-item {
|
||
width: 256px;
|
||
height: 56px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
.text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.time {
|
||
// width: 225px;
|
||
// height: 40px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #000000;
|
||
// line-height: 20px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
|
||
.alarm {
|
||
// width: 225px;
|
||
// height: 40px;
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
color: #000000;
|
||
// line-height: 20px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.line {
|
||
width: 2px;
|
||
height: 34px;
|
||
background: #FF760B;
|
||
border-radius: 1px;
|
||
}
|
||
}
|
||
|
||
/* 更具体的选择器 */
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
.el-tooltip__popper.is-light.no-border-tooltip {
|
||
border: none !important;
|
||
border-width: 0 !important;
|
||
padding: 12px 16px;
|
||
box-shadow: 0px 2px 6px 0px #D7D7D7 !important;
|
||
}
|
||
|
||
// 全局公共样式
|
||
.energyOverlimitLog {
|
||
.searchBarBox {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.blue-block {
|
||
float: left;
|
||
display: inline-block;
|
||
width: 4px;
|
||
height: 16px;
|
||
background-color: #0b58ff;
|
||
border-radius: 1px;
|
||
margin-right: 8px;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.tip {
|
||
display: inline-block;
|
||
font-size: 16px;
|
||
margin-right: 8px;
|
||
// margin-top: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
}
|
||
</style>
|