<!-- e:\VSprojects\fume-supervision-vue\src\components\monitor\RealTimeData.vue -->
|
<template>
|
<el-card class="section" shadow="hover">
|
<template #header>
|
<div class="card-header">
|
<span>设备实时数据</span>
|
</div>
|
</template>
|
<el-space v-if="devices && devices.length > 0" justify="center" wrap>
|
<el-card
|
v-for="(device, index) in devices"
|
:key="device.deviceId"
|
class="device-card"
|
shadow="hover"
|
:class="{ 'abnormal-device': isDeviceAbnormal(device) }"
|
>
|
<template #header>
|
<div class="device-header">
|
<span class="device-id">{{ device.deviceId }}</span>
|
<div class="device-status">
|
<el-icon v-if="device.status === '正常'" class="status-icon normal-icon"
|
><iconify-icon icon="mdi:check-circle"
|
/></el-icon>
|
<el-icon v-else class="status-icon abnormal-icon"
|
><iconify-icon icon="mdi:alert-circle"
|
/></el-icon>
|
<span>{{ device.status }}</span>
|
</div>
|
</div>
|
</template>
|
|
<!-- 实时分钟数据 -->
|
<div class="realtime-data">
|
<div class="basic-info">
|
<span class="supplier">{{ device.supplier }}</span>
|
<span class="monitor-time">{{
|
device.monitorTime || new Date().toLocaleString()
|
}}</span>
|
</div>
|
<div class="monitor-values">
|
<div
|
class="value-item"
|
:class="{ 'abnormal-value': isAbnormal('smokeDensity', device.smokeDensity) }"
|
>
|
<span class="value-label">油烟浓度</span>
|
<span class="value">{{ device.smokeDensity }} <span class="unit">mg/m³</span></span>
|
</div>
|
<div
|
class="value-item"
|
:class="{ 'abnormal-value': isAbnormal('fanCurrent', device.fanCurrent) }"
|
>
|
<span class="value-label">风机电流</span>
|
<span class="value">{{ device.fanCurrent }} <span class="unit">A</span></span>
|
</div>
|
<div
|
class="value-item"
|
:class="{ 'abnormal-value': isAbnormal('purifierCurrent', device.purifierCurrent) }"
|
>
|
<span class="value-label">净化器电流</span>
|
<span class="value">{{ device.purifierCurrent }} <span class="unit">A</span></span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 近一小时数据 -->
|
<div class="hourly-charts">
|
<el-popover
|
placement="left"
|
:width="600"
|
trigger="click"
|
@show="() => initDeviceCharts(device)"
|
>
|
<template #reference>
|
<div class="chart-header">
|
<span class="date">{{ new Date().toLocaleDateString() }}</span>
|
<el-button size="small" type="primary" link> 查看近一小时数据 </el-button>
|
</div>
|
</template>
|
<div class="popover-content">
|
<div class="popover-header">
|
<h3>{{ device.deviceId }} 近一小时数据</h3>
|
</div>
|
<div ref="charts" :key="device.deviceId" class="charts-container">
|
<div class="chart-item">
|
<div class="chart-title">油烟浓度(mg/m³)</div>
|
<div ref="smokeChart" :data-device-id="device.deviceId" class="small-chart"></div>
|
</div>
|
<div class="chart-item">
|
<div class="chart-title">风机电流(A)</div>
|
<div ref="fanChart" :data-device-id="device.deviceId" class="small-chart"></div>
|
</div>
|
<div class="chart-item">
|
<div class="chart-title">净化器电流(A)</div>
|
<div
|
ref="purifierChart"
|
:data-device-id="device.deviceId"
|
class="small-chart"
|
></div>
|
</div>
|
</div>
|
</div>
|
</el-popover>
|
</div>
|
</el-card>
|
</el-space>
|
<div v-else class="no-data">
|
<el-empty description="暂无设备数据" />
|
</div>
|
</el-card>
|
</template>
|
|
<script>
|
import * as echarts from 'echarts'
|
import { Icon } from '@iconify/vue'
|
|
export default {
|
name: 'RealTimeData',
|
components: {
|
IconifyIcon: Icon,
|
},
|
props: {
|
devices: {
|
type: Array,
|
default: () => [],
|
},
|
},
|
data() {
|
return {
|
charts: {},
|
}
|
},
|
mounted() {
|
// 不需要自动初始化图表,只在弹出框显示时初始化
|
},
|
beforeUnmount() {
|
Object.values(this.charts).forEach((chart) => {
|
if (chart) {
|
chart.dispose()
|
}
|
})
|
},
|
watch: {
|
devices: {
|
handler(newDevices) {
|
// 清除旧图表
|
Object.values(this.charts).forEach((chart) => {
|
if (chart) {
|
chart.dispose()
|
}
|
})
|
this.charts = {}
|
},
|
deep: true,
|
},
|
},
|
methods: {
|
initDeviceCharts(device) {
|
const deviceId = device.deviceId
|
|
// 清除该设备的旧图表
|
Object.keys(this.charts).forEach((key) => {
|
if (key.startsWith(deviceId)) {
|
this.charts[key].dispose()
|
delete this.charts[key]
|
}
|
})
|
|
this.$nextTick(() => {
|
// 油烟浓度图表
|
const smokeChartEl = document.querySelector(`[data-device-id="${deviceId}"]`)
|
if (smokeChartEl) {
|
this.charts[`${deviceId}_smoke`] = echarts.init(smokeChartEl)
|
}
|
|
// 风机电流图表
|
const fanChartEl = document.querySelectorAll(`[data-device-id="${deviceId}"]`)[1]
|
if (fanChartEl) {
|
this.charts[`${deviceId}_fan`] = echarts.init(fanChartEl)
|
}
|
|
// 净化器电流图表
|
const purifierChartEl = document.querySelectorAll(`[data-device-id="${deviceId}"]`)[2]
|
if (purifierChartEl) {
|
this.charts[`${deviceId}_purifier`] = echarts.init(purifierChartEl)
|
}
|
|
this.updateDeviceCharts(device)
|
})
|
},
|
updateCharts() {
|
this.devices.forEach((device) => {
|
this.updateDeviceCharts(device)
|
})
|
},
|
updateDeviceCharts(device) {
|
const deviceId = device.deviceId
|
const deviceHourlyData = device.hourlyData || []
|
|
const _baseOption = {
|
tooltip: {
|
trigger: 'axis',
|
},
|
grid: {
|
left: '0%',
|
right: '0%',
|
top: '10%',
|
bottom: '0%',
|
containLabel: true,
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: deviceHourlyData.map((item) => item.time),
|
axisLabel: {
|
fontSize: 10,
|
},
|
},
|
}
|
|
// 更新油烟浓度图表
|
if (this.charts[`${deviceId}_smoke`]) {
|
const smokeOption = {
|
..._baseOption,
|
yAxis: {
|
type: 'value',
|
name: 'mg/m³',
|
// nameLocation: 'middle',
|
// nameGap: 30,
|
axisLabel: {
|
fontSize: 10,
|
},
|
},
|
series: [
|
{
|
name: '油烟浓度',
|
type: 'line',
|
data: deviceHourlyData.map((item) => parseFloat(item.smokeDensity)),
|
smooth: true,
|
},
|
],
|
}
|
this.charts[`${deviceId}_smoke`].setOption(smokeOption)
|
}
|
|
// 更新风机电流图表
|
if (this.charts[`${deviceId}_fan`]) {
|
const fanOption = {
|
..._baseOption,
|
yAxis: {
|
type: 'value',
|
name: 'A',
|
// nameLocation: 'middle',
|
// nameGap: 30,
|
axisLabel: {
|
fontSize: 10,
|
},
|
},
|
series: [
|
{
|
name: '风机电流',
|
type: 'line',
|
data: deviceHourlyData.map((item) => parseFloat(item.fanCurrent)),
|
smooth: true,
|
},
|
],
|
}
|
this.charts[`${deviceId}_fan`].setOption(fanOption)
|
}
|
|
// 更新净化器电流图表
|
if (this.charts[`${deviceId}_purifier`]) {
|
const purifierOption = {
|
..._baseOption,
|
yAxis: {
|
type: 'value',
|
name: 'A',
|
// nameLocation: 'middle',
|
// nameGap: 30,
|
axisLabel: {
|
fontSize: 10,
|
},
|
},
|
series: [
|
{
|
name: '净化器电流',
|
type: 'line',
|
data: deviceHourlyData.map((item) => parseFloat(item.purifierCurrent)),
|
smooth: true,
|
},
|
],
|
}
|
this.charts[`${deviceId}_purifier`].setOption(purifierOption)
|
}
|
},
|
isDeviceAbnormal(device) {
|
return (
|
this.isAbnormal('smokeDensity', device.smokeDensity) ||
|
this.isAbnormal('fanCurrent', device.fanCurrent) ||
|
this.isAbnormal('purifierCurrent', device.purifierCurrent)
|
)
|
},
|
isAbnormal(type, value) {
|
// 这里可以根据实际情况定义异常值判断逻辑
|
const thresholds = {
|
smokeDensity: 10,
|
fanCurrent: 5,
|
purifierCurrent: 3,
|
}
|
return parseFloat(value) > (thresholds[type] || 0)
|
},
|
},
|
}
|
</script>
|
|
<style scoped lang="scss">
|
.section {
|
margin-bottom: 20px;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.device-card {
|
// width: 100%;
|
margin-bottom: 20px;
|
transition: all 0.3s ease;
|
}
|
|
.device-card:hover {
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
}
|
|
.abnormal-device {
|
border-bottom: 4px solid #f56c6c;
|
}
|
|
.device-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.device-id {
|
font-size: 16px;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.device-status {
|
display: flex;
|
align-items: center;
|
gap: 5px;
|
}
|
|
.status-icon {
|
font-size: 16px;
|
}
|
|
.normal-icon {
|
color: #67c23a;
|
}
|
|
.abnormal-icon {
|
color: #f56c6c;
|
}
|
|
.realtime-data {
|
margin-bottom: 20px;
|
}
|
|
.basic-info {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 15px;
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.supplier {
|
font-style: italic;
|
}
|
|
.monitor-time {
|
font-family: monospace;
|
}
|
|
.monitor-values {
|
display: flex;
|
justify-content: space-around;
|
gap: 10px;
|
}
|
|
.value-item {
|
flex: 1;
|
width: 60px;
|
text-align: center;
|
padding: 10px;
|
border-radius: 4px;
|
background-color: #f9f9f9;
|
border: 1px solid transparent;
|
}
|
|
.value-label {
|
display: block;
|
font-size: 12px;
|
color: #606266;
|
margin-bottom: 5px;
|
}
|
|
.value {
|
display: block;
|
font-size: 20px;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.unit {
|
font-size: 12px;
|
font-weight: normal;
|
color: #909399;
|
}
|
|
.abnormal-value {
|
background-color: #fef0f0;
|
border: 1px solid #fbc4c4;
|
}
|
|
.abnormal-value .value {
|
color: #f56c6c;
|
}
|
|
.hourly-charts {
|
margin-top: 20px;
|
}
|
|
.chart-header {
|
margin-top: 10px;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 5px 0;
|
}
|
|
.popover-content {
|
padding: 10px;
|
}
|
|
.popover-header {
|
margin-bottom: 15px;
|
border-bottom: 1px solid #f0f0f0;
|
padding-bottom: 10px;
|
}
|
|
.popover-header h3 {
|
margin: 0;
|
font-size: 14px;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.date {
|
font-size: 12px;
|
color: #909399;
|
font-family: monospace;
|
}
|
|
.charts-container {
|
display: flex;
|
flex-direction: column;
|
gap: 2px;
|
}
|
|
.chart-item {
|
background-color: #f9f9f9;
|
border-radius: 4px;
|
padding: 10px;
|
}
|
|
.chart-title {
|
font-size: 12px;
|
color: #606266;
|
margin-bottom: 5px;
|
text-align: center;
|
}
|
|
.small-chart {
|
height: 120px;
|
width: 100%;
|
}
|
|
.no-data {
|
padding: 40px 0;
|
text-align: center;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 768px) {
|
.monitor-values {
|
flex-direction: column;
|
}
|
|
.value-item {
|
width: 100%;
|
}
|
|
.small-chart {
|
height: 100px;
|
}
|
}
|
</style>
|