餐饮油烟智能监测与监管一体化平台
feiyu02
2026-03-20 20cdb83586daabfb15fc056c4c97eb8e7ccaf928
src/components/monitor/RealTimeData.vue
@@ -6,153 +6,313 @@
        <span>设备实时数据</span>
      </div>
    </template>
    <el-row :gutter="20">
      <el-col :span="8">
        <el-card class="realtime-data" shadow="hover">
          <template #header>
            <span>实时分钟数据</span>
          </template>
          <el-descriptions :column="1" v-if="currentDevice">
            <el-descriptions-item label="设备编号">{{
              currentDevice.deviceId
            }}</el-descriptions-item>
            <el-descriptions-item label="设备供应商">{{
              currentDevice.supplier
            }}</el-descriptions-item>
            <el-descriptions-item label="油烟浓度"
              >{{ currentDevice.油烟浓度 }} mg/m³</el-descriptions-item
    <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) }"
            >
            <el-descriptions-item label="风机电流"
              >{{ currentDevice.风机电流 }} A</el-descriptions-item
              <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) }"
            >
            <el-descriptions-item label="净化器电流"
              >{{ currentDevice.净化器电流 }} A</el-descriptions-item
              <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) }"
            >
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="16">
        <el-card class="realtime-chart" shadow="hover">
          <template #header>
            <span>近一小时数据</span>
          </template>
          <div ref="hourlyChart" class="chart-container"></div>
        </el-card>
      </el-col>
    </el-row>
              <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: {
    currentDevice: {
      type: Object,
      default: null,
    },
    hourlyData: {
    devices: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      hourlyChart: null,
      charts: {},
    }
  },
  mounted() {
    this.initChart()
    // 不需要自动初始化图表,只在弹出框显示时初始化
  },
  beforeUnmount() {
    if (this.hourlyChart) {
      this.hourlyChart.dispose()
    }
    Object.values(this.charts).forEach((chart) => {
      if (chart) {
        chart.dispose()
      }
    })
  },
  watch: {
    hourlyData() {
      this.updateChart()
    devices: {
      handler(newDevices) {
        // 清除旧图表
        Object.values(this.charts).forEach((chart) => {
          if (chart) {
            chart.dispose()
          }
        })
        this.charts = {}
      },
      deep: true,
    },
  },
  methods: {
    initChart() {
      this.hourlyChart = echarts.init(this.$refs.hourlyChart)
      this.updateChart()
    initDeviceCharts(device) {
      const deviceId = device.deviceId
      window.addEventListener('resize', () => {
        this.hourlyChart.resize()
      // 清除该设备的旧图表
      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)
      })
    },
    updateChart() {
      if (!this.hourlyChart) return
    updateCharts() {
      this.devices.forEach((device) => {
        this.updateDeviceCharts(device)
      })
    },
    updateDeviceCharts(device) {
      const deviceId = device.deviceId
      const deviceHourlyData = device.hourlyData || []
      const option = {
      const _baseOption = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            label: {
              backgroundColor: '#6a7985',
            },
          },
        },
        legend: {
          data: ['油烟浓度', '风机电流', '净化器电流'],
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          left: '0%',
          right: '0%',
          top: '10%',
          bottom: '0%',
          containLabel: true,
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: this.hourlyData.map((item) => item.time),
          data: deviceHourlyData.map((item) => item.time),
          axisLabel: {
            fontSize: 10,
          },
        },
        yAxis: [
          {
            type: 'value',
            name: '油烟浓度 (mg/m³)',
            position: 'left',
          },
          {
            type: 'value',
            name: '电流 (A)',
            position: 'right',
          },
        ],
        series: [
          {
            name: '油烟浓度',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.油烟浓度)),
            yAxisIndex: 0,
          },
          {
            name: '风机电流',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.风机电流)),
            yAxisIndex: 1,
          },
          {
            name: '净化器电流',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.净化器电流)),
            yAxisIndex: 1,
          },
        ],
      }
      this.hourlyChart.setOption(option)
      // 更新油烟浓度图表
      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>
<style scoped lang="scss">
.section {
  margin-bottom: 20px;
}
@@ -163,8 +323,191 @@
  align-items: center;
}
.chart-container {
  height: 300px;
.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>