소스 검색

fix:device-monitor

0224986 2 일 전
부모
커밋
b02483be18

+ 39 - 65
industry-monitor-web/src/app/pages/device-monitor/device-monitor.component.html

@@ -1,37 +1,41 @@
-
 <!-- device-monitor.component.html -->
 <div class="monitor-container">
   <div class="monitor-header">
     <h2><i class="fa fa-microchip"></i> 单设备监测面板</h2>
-    <div class="device-info">
-      <div><strong>设备名称:</strong> CNC 铣床 #01</div>
-      <div><strong>设备编号:</strong> DEV-2023-001</div>
-      <div><strong>位置:</strong> 车间A - 产线3</div>
-      <div><strong>设备类型:</strong> CNC加工中心</div>
+    <div class="header-info">
+      <div class="device-info">
+        <div><strong>设备名称:</strong> CNC 铣床 #01</div>
+        <div><strong>设备编号:</strong> DEV-2023-001</div>
+        <div><strong>位置:</strong> 车间A - 产线3</div>
+        <div><strong>设备类型:</strong> CNC加工中心</div>
+      </div>
+      <div class="current-time">
+        <strong>当前时间:</strong> {{ currentTime | date: 'yyyy-MM-dd HH:mm:ss' }}
+      </div>
     </div>
   </div>
 
   <div class="monitor-row">
     <div class="waveform-card">
       <h3><i class="fa fa-wave-square"></i> 振动波形图 (40秒周期)</h3>
-      <div class="waveform-container">
-        <div class="waveform-grid">
-          <div class="grid-line"></div>
-          <div class="grid-line"></div>
-          <div class="grid-line"></div>
-          <div class="waveform-line"></div>
-          <div class="threshold upper"></div>
-          <div class="threshold lower"></div>
-        </div>
+      <!-- 修改1: 使用 appEchart 指令和 echartOptions 绑定 -->
+      <div class="waveform-container" 
+           appEchart 
+           [echartOptions]="chartOptions">
       </div>
       <div class="waveform-controls">
         <div class="threshold-control">
-          <label>阈值上限: <span id="upperValue">0.8</span> mm/s</label>
-          <input type="range" min="0" max="1.5" step="0.1" value="0.8" id="upperThreshold">
+          <label>阈值上限: {{ upperThreshold }} mm/s</label>
+          <!-- 修改2: 更新滑块绑定 -->
+          <input type="range" min="0.1" max="1.5" step="0.05" 
+                 [(ngModel)]="upperThreshold" 
+                 (input)="updateThreshold()">
         </div>
         <div class="threshold-control">
-          <label>阈值下限: <span id="lowerValue">0.2</span> mm/s</label>
-          <input type="range" min="0" max="1.5" step="0.1" value="0.2" id="lowerThreshold">
+          <label>阈值下限: {{ lowerThreshold }} mm/s</label>
+          <input type="range" min="0.1" max="1.5" step="0.05" 
+                 [(ngModel)]="lowerThreshold" 
+                 (input)="updateThreshold()">
         </div>
       </div>
     </div>
@@ -39,40 +43,14 @@
     <div class="status-panel">
       <h3><i class="fa fa-heartbeat"></i> 实时状态</h3>
       <div class="status-grid">
-        <div class="status-card">
-          <div class="status-icon blue">
-            <i class="fa fa-wave-square"></i>
-          </div>
-          <div class="status-data">
-            <div class="status-label">当前振幅</div>
-            <div class="status-value">0.58 mm/s</div>
-          </div>
-        </div>
-        <div class="status-card">
-          <div class="status-icon green">
-            <i class="fa fa-heartbeat"></i>
+        <!-- 修改3: 修复 ngClass 绑定 -->
+        <div class="status-card" *ngFor="let status of statusValues">
+          <div class="status-icon" [ngClass]="status.color">
+            <i class="fa" [ngClass]="status.icon"></i>
           </div>
           <div class="status-data">
-            <div class="status-label">健康指数</div>
-            <div class="status-value">89%</div>
-          </div>
-        </div>
-        <div class="status-card">
-          <div class="status-icon orange">
-            <i class="fa fa-thermometer-half"></i>
-          </div>
-          <div class="status-data">
-            <div class="status-label">温度</div>
-            <div class="status-value">68°C</div>
-          </div>
-        </div>
-        <div class="status-card">
-          <div class="status-icon purple">
-            <i class="fa fa-tachometer-alt"></i>
-          </div>
-          <div class="status-data">
-            <div class="status-label">转速</div>
-            <div class="status-value">1420 RPM</div>
+            <div class="status-label">{{ status.label }}</div>
+            <div class="status-value">{{ status.value }} {{ status.unit }}</div>
           </div>
         </div>
       </div>
@@ -102,20 +80,16 @@
     </div>
     
     <div class="alert-history">
-      <div class="history-item">
-        <div class="alert-time">14:30:22</div>
-        <div class="alert-desc">振动超标 (0.92 mm/s)</div>
-        <div class="alert-level warning">警告</div>
-      </div>
-      <div class="history-item">
-        <div class="alert-time">11:45:10</div>
-        <div class="alert-desc">温度异常 (78°C)</div>
-        <div class="alert-level critical">严重</div>
-      </div>
-      <div class="history-item">
-        <div class="alert-time">09:15:33</div>
-        <div class="alert-desc">转速波动超出范围</div>
-        <div class="alert-level info">注意</div>
+      <!-- 修改4: 修复 ngClass 绑定 -->
+      <div class="history-item" *ngFor="let alert of alertHistory">
+        <div class="alert-time">{{ alert.time }}</div>
+        <div class="alert-desc">{{ alert.desc }}</div>
+        <div class="alert-level" [ngClass]="alert.level">
+          {{ 
+            alert.level === 'warning' ? '警告' : 
+            alert.level === 'critical' ? '严重' : '注意' 
+          }}
+        </div>
       </div>
     </div>
   </div>

+ 152 - 9
industry-monitor-web/src/app/pages/device-monitor/device-monitor.component.ts

@@ -1,21 +1,164 @@
-import { Component, ViewEncapsulation } from '@angular/core';
+import { Component, ViewEncapsulation, OnInit } from '@angular/core';
 import { DatePipe } from '@angular/common';
+import * as echarts from 'echarts';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { EchartDirective } from '../../shared/directives/echarts.directive';
 
 @Component({
   standalone: true,
   selector: 'app-device-monitor',
-  styleUrls: ['./device-monitor.css'],
-  imports: [DatePipe],
   templateUrl: './device-monitor.component.html',
+  styleUrls: ['./device-monitor.css'],
+  imports: [
+    DatePipe,
+    CommonModule,
+    FormsModule,
+    EchartDirective
+  ],
   encapsulation: ViewEncapsulation.None
 })
-export class DeviceMonitorComponent {
-currentTime: Date = new Date();
-   ngOnInit(): void {
-    // 更新时间,每秒更新一次
+export class DeviceMonitorComponent implements OnInit {
+  currentTime: Date = new Date();
+  vibrationData: number[] = [];
+  upperThreshold = 0.8;
+  lowerThreshold = 0.2;
+  currentAmplitude = 0.58;
+  healthIndex = 89;
+  temperature = 68;
+  rotationSpeed = 1420;
+  chartOptions: any;
+  
+  statusValues = [
+    { label: '当前振幅', value: 0.58, unit: 'mm/s', icon: 'fa-wave-square', color: 'blue' },
+    { label: '健康指数', value: 89, unit: '%', icon: 'fa-heartbeat', color: 'green' },
+    { label: '温度', value: 68, unit: '°C', icon: 'fa-thermometer-half', color: 'orange' },
+    { label: '转速', value: 1420, unit: 'RPM', icon: 'fa-tachometer-alt', color: 'purple' }
+  ];
+  
+  alertHistory = [
+    { time: '14:30:22', desc: '振动超标 (0.92 mm/s)', level: 'warning' },
+    { time: '11:45:10', desc: '温度异常 (78°C)', level: 'critical' },
+    { time: '09:15:33', desc: '转速波动超出范围', level: 'info' }
+  ];
+
+  ngOnInit(): void {
     setInterval(() => {
       this.currentTime = new Date();
     }, 1000);
 
-   }
-}
+    this.generateVibrationData();
+    this.updateChartOptions();
+    
+    setInterval(() => {
+      this.updateSensorData();
+    }, 3000);
+  }
+
+  generateVibrationData(): void {
+    for (let i = 0; i < 200; i++) {
+      const baseValue = Math.sin(i * 0.2) * 0.5;
+      const noise = (Math.random() - 0.5) * 0.2;
+      this.vibrationData.push(baseValue + noise + 0.5);
+    }
+  }
+
+  updateSensorData(): void {
+    this.currentAmplitude = 0.4 + Math.random() * 0.5;
+    this.healthIndex = Math.max(70, Math.min(95, this.healthIndex + (Math.random() - 0.5) * 5));
+    this.temperature = Math.max(60, Math.min(75, this.temperature + (Math.random() - 0.5) * 2));
+    this.rotationSpeed = 1400 + Math.floor(Math.random() * 50);
+    
+    this.statusValues[0].value = parseFloat(this.currentAmplitude.toFixed(2));
+    this.statusValues[1].value = Math.round(this.healthIndex);
+    this.statusValues[2].value = Math.round(this.temperature);
+    this.statusValues[3].value = this.rotationSpeed;
+    
+    // 更新振动数据
+    this.vibrationData.shift();
+    const newValue = 0.4 + Math.random() * 0.6;
+    this.vibrationData.push(newValue);
+    
+    // 更新图表
+    this.updateChartOptions();
+  }
+
+  updateChartOptions(): void {
+    this.chartOptions = {
+      tooltip: {
+        trigger: 'axis',
+        formatter: (params: any) => {
+          const value = params[0].value;
+          let status = '正常';
+          if (value > this.upperThreshold) status = '超标';
+          if (value < this.lowerThreshold) status = '过低';
+          
+          return `时间: ${params[0].name}<br/>振幅: ${value.toFixed(2)} mm/s<br/>状态: ${status}`;
+        }
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '12%',
+        top: '10%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: Array.from({length: 200}, (_, i) => `${i*0.2}s`),
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: { show: false }
+      },
+      yAxis: {
+        type: 'value',
+        min: 0,
+        max: 1.5,
+        splitLine: {
+          lineStyle: { color: 'rgba(0, 0, 0, 0.05)' }
+        },
+        axisLabel: { formatter: '{value} mm/s' }
+      },
+      series: [
+        {
+          name: '振动波形',
+          type: 'line',
+          smooth: true,
+          symbol: 'none',
+          sampling: 'average',
+          data: this.vibrationData,
+          lineStyle: { width: 2, color: '#3498db' },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(52, 152, 219, 0.3)' },
+              { offset: 1, color: 'rgba(52, 152, 219, 0.1)' }
+            ])
+          },
+          markLine: {
+            silent: true,
+            symbol: 'none',
+            lineStyle: { color: '#e74c3c', width: 1, type: 'dashed' },
+            data: [
+              {
+                name: '阈值上限',
+                yAxis: this.upperThreshold,
+                label: { formatter: '上限: {c} mm/s', position: 'end' }
+              },
+              {
+                name: '阈值下限',
+                yAxis: this.lowerThreshold,
+                label: { formatter: '下限: {c} mm/s', position: 'end' }
+              }
+            ]
+          }
+        }
+      ],
+      animation: false
+    };
+  }
+
+  updateThreshold(): void {
+    this.updateChartOptions();
+  }
+}

+ 115 - 51
industry-monitor-web/src/app/pages/device-monitor/device-monitor.css

@@ -1,8 +1,9 @@
-/* device-monitor.component.css */
+device-monitor.component.css
 :host {
   display: block;
   width: 100%;
   padding: 15px;
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 }
 
 .monitor-container {
@@ -31,6 +32,12 @@
   color: #2c3e50;
 }
 
+.header-info {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+}
+
 .device-info {
   display: grid;
   grid-template-columns: repeat(2, 1fr);
@@ -39,6 +46,15 @@
   background: #f8fafc;
   padding: 15px;
   border-radius: 6px;
+  margin-bottom: 10px;
+}
+
+.current-time {
+  font-size: 14px;
+  background: #f8fafc;
+  padding: 8px 15px;
+  border-radius: 4px;
+  font-weight: 500;
 }
 
 .monitor-row {
@@ -47,12 +63,20 @@
   margin-bottom: 20px;
 }
 
+@media (max-width: 1200px) {
+  .monitor-row {
+    flex-direction: column;
+  }
+}
+
 .waveform-card {
   flex: 2;
   background: white;
   border-radius: 8px;
   padding: 20px;
   box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  display: flex;
+  flex-direction: column;
 }
 
 .status-panel {
@@ -61,66 +85,24 @@
   border-radius: 8px;
   padding: 20px;
   box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  display: flex;
+  flex-direction: column;
 }
 
 .waveform-container {
   height: 250px;
-  background: #f1f8ff;
+  background: #f8fafc;
   border-radius: 6px;
-  margin: 20px 0;
+  margin: 15px 0;
   position: relative;
   overflow: hidden;
+  border: 1px solid #eee;
 }
 
-.waveform-grid {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-}
-
-.grid-line {
-  position: absolute;
-  height: 1px;
-  background: rgba(0, 0, 0, 0.05);
-  left: 0;
-  right: 0;
-}
-
-.grid-line:nth-child(1) { top: 25%; }
-.grid-line:nth-child(2) { top: 50%; }
-.grid-line:nth-child(3) { top: 75%; }
-
-.waveform-line {
-  position: absolute;
-  top: 50%;
-  left: 0;
-  right: 0;
-  height: 3px;
-  background: #3498db;
-  animation: waveform 8s infinite linear;
-}
-
-@keyframes waveform {
-  0% { transform: translateX(0); }
-  100% { transform: translateX(-100%); }
-}
-
-.threshold {
-  position: absolute;
-  left: 0;
-  right: 0;
-  height: 2px;
-  background: #e74c3c;
-}
-
-.threshold.upper { top: 25%; }
-.threshold.lower { top: 75%; }
-
 .waveform-controls {
   display: flex;
   gap: 20px;
+  margin-top: 15px;
 }
 
 .threshold-control {
@@ -131,10 +113,26 @@
   display: block;
   margin-bottom: 8px;
   font-size: 14px;
+  font-weight: 500;
 }
 
 .threshold-control input {
   width: 100%;
+  height: 6px;
+  border-radius: 3px;
+  background: #e0e7ff;
+  outline: none;
+  -webkit-appearance: none;
+}
+
+.threshold-control input::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: #3498db;
+  cursor: pointer;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
 }
 
 .status-grid {
@@ -150,8 +148,14 @@
   gap: 15px;
   padding: 15px;
   background: #f8fafc;
-  border-radius: 6px;
-  border-left: 3px solid #3498db;
+  border-radius: 8px;
+  transition: all 0.3s ease;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+}
+
+.status-card:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
 }
 
 .status-icon {
@@ -163,6 +167,7 @@
   justify-content: center;
   font-size: 18px;
   color: white;
+  flex-shrink: 0;
 }
 
 .status-icon.blue { background: #3498db; }
@@ -186,6 +191,10 @@
   color: #2c3e50;
 }
 
+.progress-container {
+  margin-top: auto;
+}
+
 .progress-steps {
   display: flex;
   justify-content: space-between;
@@ -202,6 +211,23 @@
   justify-content: center;
   font-weight: 700;
   color: #95a5a6;
+  position: relative;
+  transition: all 0.3s ease;
+}
+
+.step::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 100%;
+  width: 20px;
+  height: 2px;
+  background: #ecf0f1;
+  z-index: 1;
+}
+
+.step:last-child::after {
+  display: none;
 }
 
 .step.active {
@@ -209,10 +235,15 @@
   color: white;
 }
 
+.step.active::after {
+  background: #3498db;
+}
+
 .step.current {
   background: #27ae60;
   color: white;
   transform: scale(1.2);
+  box-shadow: 0 0 0 4px rgba(39, 174, 96, 0.3);
 }
 
 .history-panel {
@@ -229,6 +260,30 @@
   margin-bottom: 15px;
 }
 
+.controls {
+  display: flex;
+  gap: 10px;
+}
+
+.btn {
+  padding: 8px 15px;
+  border-radius: 4px;
+  border: none;
+  background: #3498db;
+  color: white;
+  font-weight: 500;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  transition: all 0.2s;
+}
+
+.btn:hover {
+  background: #2980b9;
+  transform: translateY(-2px);
+}
+
 .alert-history {
   border: 1px solid #eee;
   border-radius: 6px;
@@ -240,6 +295,11 @@
   align-items: center;
   padding: 12px 15px;
   border-bottom: 1px solid #eee;
+  transition: background 0.2s;
+}
+
+.history-item:hover {
+  background: #f9f9f9;
 }
 
 .history-item:last-child {
@@ -250,6 +310,7 @@
   width: 90px;
   font-weight: 500;
   color: #2c3e50;
+  flex-shrink: 0;
 }
 
 .alert-desc {
@@ -261,6 +322,9 @@
   border-radius: 4px;
   font-size: 13px;
   font-weight: 500;
+  width: 60px;
+  text-align: center;
+  flex-shrink: 0;
 }
 
 .alert-level.warning {

+ 36 - 0
industry-monitor-web/src/app/shared/directives/echarts.directive.ts

@@ -0,0 +1,36 @@
+// src/app/shared/directives/echarts.directive.ts
+import { Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';
+import * as echarts from 'echarts';
+
+@Directive({
+  selector: '[appEchart]',
+  standalone: true
+})
+export class EchartDirective implements OnChanges {
+  @Input() echartOptions: any;
+  private chart: any;
+
+  constructor(private el: ElementRef) {}
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes['echartOptions']) {
+      this.renderChart();
+    }
+  }
+
+  private renderChart() {
+    if (!this.chart) {
+      this.chart = echarts.init(this.el.nativeElement);
+    }
+    
+    if (this.echartOptions) {
+      this.chart.setOption(this.echartOptions);
+    }
+  }
+
+  ngOnDestroy() {
+    if (this.chart) {
+      this.chart.dispose();
+    }
+  }
+}