From 20cdb83586daabfb15fc056c4c97eb8e7ccaf928 Mon Sep 17 00:00:00 2001
From: feiyu02 <risaku@163.com>
Date: 星期五, 20 三月 2026 17:07:16 +0800
Subject: [PATCH] 2026.3.20

---
 src/components/monitor/RealTimeData.vue |  543 ++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 443 insertions(+), 100 deletions(-)

diff --git a/src/components/monitor/RealTimeData.vue b/src/components/monitor/RealTimeData.vue
index f8aba0a..8e3e361 100644
--- a/src/components/monitor/RealTimeData.vue
+++ b/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 === '姝e父'" 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>

--
Gitblit v1.9.3