瀏覽代碼

feat: chart

0235953 2 天之前
父節點
當前提交
9ba4f0dc8f

+ 63 - 0
industry-device/components/event-table/event-table.component.html

@@ -0,0 +1,63 @@
+<!-- src/app/components/event-table/event-table.component.html -->
+<div class="event-table-container">
+  <div class="table-header">
+    <h3>事件记录</h3>
+    <div class="table-actions">
+      <button class="btn btn-sm btn-export" (click)="exportCSV()">
+        <i class="fas fa-download"></i> 导出CSV
+      </button>
+    </div>
+  </div>
+
+  <div class="table-responsive">
+    <table class="event-table">
+      <thead>
+        <tr>
+          <th>时间</th>
+          <th>事件类型</th>
+          <th>设备</th>
+          <th>振动值(g)</th>
+          <th>持续时间</th>
+          <th>操作</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let event of paginatedEvents">
+          <td>{{ event.eventTime | date:'yyyy-MM-dd HH:mm:ss' }}</td>
+          <td>
+            <span class="event-tag" [class]="getEventClass(event.eventType)">
+              {{ event.eventType }}
+            </span>
+          </td>
+          <td>{{ event.deviceId }}</td>
+          <td>{{ event.vibrationValue | number:'1.1-1' }}</td>
+          <td>{{ event.duration }}</td>
+          <td>
+            <button class="btn btn-sm btn-details" (click)="showDetails(event)">
+              <i class="fas fa-info-circle"></i> 详情
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <!-- 分页控件 -->
+  <div class="pagination-controls" *ngIf="events.length > itemsPerPage">
+    <button class="btn btn-prev" 
+            [disabled]="currentPage === 1" 
+            (click)="changePage(currentPage - 1)">
+      上一页
+    </button>
+    
+    <span class="page-info">
+      第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
+    </span>
+    
+    <button class="btn btn-next" 
+            [disabled]="currentPage === totalPages" 
+            (click)="changePage(currentPage + 1)">
+      下一页
+    </button>
+  </div>
+</div>

+ 93 - 0
industry-device/components/event-table/event-table.component.scss

@@ -0,0 +1,93 @@
+// src/app/components/event-table/event-table.component.scss
+//@import 'src/styles/variables';
+
+.event-table-container {
+  //background: $card-bg;
+  border-radius: 8px;
+  padding: 20px;
+  margin-top: 20px;
+  //box-shadow: $shadow-sm;
+}
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  
+  h3 {
+    margin: 0;
+    color: $primary;
+  }
+}
+
+.event-table {
+  width: 100%;
+  border-collapse: collapse;
+  
+  th, td {
+    padding: 12px 15px;
+    text-align: left;
+    border-bottom: 1px solid $border-color;
+  }
+  
+  th {
+    background: rgba($primary, 0.1);
+    font-weight: 600;
+  }
+  
+  tr:hover {
+    background: rgba($primary, 0.05);
+  }
+}
+
+.event-tag {
+  display: inline-block;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  
+  &.warning {
+    background: rgba($warning, 0.2);
+    color: $warning;
+  }
+  
+  &.danger {
+    background: rgba($danger, 0.2);
+    color: $danger;
+  }
+  
+  &.success {
+    background: rgba($success, 0.2);
+    color: $success;
+  }
+  
+  &.info {
+    background: rgba($info, 0.2);
+    color: $info;
+  }
+}
+
+.pagination-controls {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 20px;
+  gap: 15px;
+  
+  .page-info {
+    margin: 0 10px;
+  }
+}
+
+.btn {
+  &-export {
+    background: $success;
+    color: white;
+  }
+  
+  &-details {
+    background: $info;
+    color: white;
+  }
+}

+ 58 - 0
industry-device/components/event-table/event-table.component.ts

@@ -0,0 +1,58 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule, DatePipe, DecimalPipe } from '@angular/common';
+
+@Component({
+  selector: 'app-event-table',
+  standalone: true,
+  imports: [CommonModule, DatePipe, DecimalPipe],
+  templateUrl: './event-table.component.html',
+  styleUrls: ['./event-table.component.scss']
+})
+export class EventTableComponent {
+  @Input() events: any[] = [];
+  @Input() currentPage = 1;
+  @Input() totalPages = 1;
+  
+  // 添加缺失的方法
+  exportCSV() {
+    // 实现CSV导出逻辑
+    const headers = ['时间', '事件类型', '设备', '振动值(g)', '持续时间'];
+    const rows = this.events.map(event => [
+      event.eventTime,
+      event.eventType,
+      event.deviceId,
+      event.vibrationValue,
+      event.duration
+    ]);
+    
+    const csvContent = [headers, ...rows].map(row => 
+      row.map(field => `"${field}"`).join(',')
+    ).join('\n');
+    
+    this.downloadFile(csvContent, '振动事件.csv');
+  }
+
+  private downloadFile(content: string, filename: string) {
+    const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
+    const link = document.createElement('a');
+    const url = URL.createObjectURL(blob);
+    
+    link.setAttribute('href', url);
+    link.setAttribute('download', filename);
+    link.style.visibility = 'hidden';
+    
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  }
+
+  showDetails(event: any) {
+    console.log('事件详情:', event);
+    alert(`事件详情:
+时间: ${event.eventTime}
+类型: ${event.eventType}
+设备: ${event.deviceId}
+振动值: ${event.vibrationValue}g
+持续时间: ${event.duration}`);
+  }
+}

+ 142 - 3
industry-device/industry-web/package-lock.json

@@ -15,6 +15,7 @@
         "@angular/platform-browser": "^20.0.0",
         "@angular/router": "^20.0.0",
         "echarts": "^5.6.0",
+        "parse": "^6.1.1",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.0"
@@ -23,7 +24,10 @@
         "@angular/build": "^20.0.4",
         "@angular/cli": "^20.0.4",
         "@angular/compiler-cli": "^20.0.0",
+        "@types/dotenv": "^6.1.1",
         "@types/jasmine": "~5.1.0",
+        "@types/node": "^24.0.10",
+        "dotenv": "^17.0.1",
         "jasmine-core": "~5.7.0",
         "karma": "~6.4.0",
         "karma-chrome-launcher": "~3.2.0",
@@ -730,6 +734,19 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@babel/runtime-corejs3": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz",
+      "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js-pure": "^3.30.2",
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.27.2",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -3243,6 +3260,16 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/dotenv": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
+      "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/estree": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -3258,9 +3285,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "24.0.8",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.8.tgz",
-      "integrity": "sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==",
+      "version": "24.0.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
+      "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -4092,6 +4119,17 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/core-js-pure": {
+      "version": "3.43.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.43.0.tgz",
+      "integrity": "sha512-i/AgxU2+A+BbJdMxh3v7/vxi2SbFqxiFmg6VsDwYB4jkucrd1BZNA9a9gphC0fYMG5IBSgQcbQnk865VCLe7xA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/cors": {
       "version": "2.8.5",
       "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -4137,6 +4175,13 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/css-select": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -4313,6 +4358,19 @@
         "url": "https://github.com/fb55/domutils?sponsor=1"
       }
     },
+    "node_modules/dotenv": {
+      "version": "17.0.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz",
+      "integrity": "sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5194,6 +5252,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/idb-keyval": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
+      "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
+      "license": "Apache-2.0"
+    },
     "node_modules/ignore-walk": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz",
@@ -7031,6 +7095,47 @@
         "node": "^20.17.0 || >=22.9.0"
       }
     },
+    "node_modules/parse": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/parse/-/parse-6.1.1.tgz",
+      "integrity": "sha512-zf70XcHKesDcqpO2RVKyIc1l7pngxBsYQVl0Yl/A38pftOSP8BQeampqqLEqMknzUetNZy8B+wrR3k5uTQDXOw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime-corejs3": "7.27.0",
+        "idb-keyval": "6.2.1",
+        "react-native-crypto-js": "1.0.0",
+        "uuid": "10.0.0",
+        "ws": "8.18.1",
+        "xmlhttprequest": "1.8.0"
+      },
+      "engines": {
+        "node": "18 || 19 || 20 || 22"
+      },
+      "optionalDependencies": {
+        "crypto-js": "4.2.0"
+      }
+    },
+    "node_modules/parse/node_modules/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/parse5": {
       "version": "7.3.0",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -7311,6 +7416,12 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/react-native-crypto-js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz",
+      "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==",
+      "license": "MIT"
+    },
     "node_modules/readdirp": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -7332,6 +7443,12 @@
       "dev": true,
       "license": "Apache-2.0"
     },
+    "node_modules/regenerator-runtime": {
+      "version": "0.14.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+      "license": "MIT"
+    },
     "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -8444,6 +8561,19 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -8800,6 +8930,15 @@
         }
       }
     },
+    "node_modules/xmlhttprequest": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
+      "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/y18n": {
       "version": "5.0.8",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

+ 4 - 0
industry-device/industry-web/package.json

@@ -27,6 +27,7 @@
     "@angular/platform-browser": "^20.0.0",
     "@angular/router": "^20.0.0",
     "echarts": "^5.6.0",
+    "parse": "^6.1.1",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"
@@ -35,7 +36,10 @@
     "@angular/build": "^20.0.4",
     "@angular/cli": "^20.0.4",
     "@angular/compiler-cli": "^20.0.0",
+    "@types/dotenv": "^6.1.1",
     "@types/jasmine": "~5.1.0",
+    "@types/node": "^24.0.10",
+    "dotenv": "^17.0.1",
     "jasmine-core": "~5.7.0",
     "karma": "~6.4.0",
     "karma-chrome-launcher": "~3.2.0",

+ 1 - 0
industry-device/industry-web/src/app/app.ts

@@ -1,6 +1,7 @@
 import { Component } from '@angular/core';
 import { RouterOutlet } from '@angular/router';
 
+
 @Component({
   selector: 'app-root',
   imports: [RouterOutlet],

+ 92 - 32
industry-device/industry-web/src/modules/industry/machine/page-vibration-history/page-vibration-history.html

@@ -38,25 +38,26 @@
   
   <div class="history-controls">
     <div class="time-range">
-      <select class="time-select">
-        <option>今天</option>
-        <option selected>最近7天</option>
-        <option>最近30天</option>
-        <option>最近90天</option>
-        <option>自定义范围</option>
+      <select class="time-select" [(ngModel)]="timeRange" (change)="onTimeRangeChange()">
+        <option value="today">今天</option>
+        <option value="7days" selected>最近7天</option>
+        <option value="30days">最近30天</option>
+        <option value="90days">最近90天</option>
+        <option value="custom">自定义范围</option>
       </select>
       
       <div class="custom-range" *ngIf="showCustomRange">
-        <input type="datetime-local">
+        <input type="datetime-local" [(ngModel)]="startDate">
         <span>至</span>
-        <input type="datetime-local">
+        <input type="datetime-local" [(ngModel)]="endDate">
+        <button class="btn small" (click)="applyCustomRange()">应用</button>
       </div>
     </div>
     
     <div class="search-filter">
-      <input type="text" placeholder="搜索事件或设备...">
-      <button class="btn">查询</button>
-      <button class="btn secondary">重置</button>
+      <input type="text" placeholder="搜索事件或设备..." [(ngModel)]="searchQuery">
+      <button class="btn" (click)="searchEvents()">查询</button>
+      <button class="btn secondary" (click)="resetSearch()">重置</button>
     </div>
   </div>
   
@@ -84,14 +85,31 @@
     <div class="table-header">
       <h3>事件记录</h3>
       <div class="table-actions">
-        <button class="btn small">导出CSV</button>
-        <button class="btn small secondary">批量操作</button>
+        <button class="btn small" (click)="exportCSV()">导出CSV</button>
+        <button class="btn small secondary" (click)="showBatchActions = !showBatchActions">
+          {{ showBatchActions ? '取消' : '批量操作' }}
+        </button>
       </div>
     </div>
     
-    <table class="event-table">
+    <div *ngIf="showBatchActions" class="batch-actions">
+      <select [(ngModel)]="batchAction">
+        <option value="">选择操作...</option>
+        <option value="delete">删除选中项</option>
+        <option value="export">导出选中项</option>
+      </select>
+      <button class="btn small" (click)="executeBatchAction()">执行</button>
+    </div>
+    
+    <div *ngIf="isLoading" class="loading-indicator">
+      <div class="spinner"></div>
+      <span>加载中...</span>
+    </div>
+    
+    <table class="event-table" *ngIf="!isLoading">
       <thead>
         <tr>
+          <th *ngIf="showBatchActions"><input type="checkbox" (change)="toggleSelectAll($event)"></th>
           <th>时间</th>
           <th>事件类型</th>
           <th>设备</th>
@@ -101,31 +119,73 @@
         </tr>
       </thead>
       <tbody>
-        <tr *ngFor="let event of events">
-          <td>{{event.time}}</td>
-          <td>
-            <span class="event-tag" [class.warning]="event.type === '振动超标'"
-                  [class.danger]="event.type === '严重故障'">
-              {{event.type}}
-            </span>
-          </td>
-          <td>{{event.device}}</td>
-          <td>{{event.value}} g</td>
-          <td>{{event.duration}}</td>
-          <td>
-            <button class="btn small">详情</button>
-          </td>
-        </tr>
+   <!-- 修复所有属性引用 -->
+    <tr *ngFor="let event of events">
+      <td>
+        <input type="checkbox" 
+              [checked]="isSelected(event.id)" 
+              (change)="toggleSelectEvent(event.id)" />
+      </td>
+      <td>{{event.eventTime | date:'yyyy-MM-dd HH:mm:ss'}}</td>
+      <td class="event-type"
+          [class.warning]="event.eventType === '振动超标' || event.eventType === '刀具磨损'"
+          [class.danger]="event.eventType === '严重故障'"
+          [class.success]="event.eventType === '正常停机'">
+          {{event.eventType}}
+      </td>
+      <td>{{event.deviceId}}</td>
+      <td>{{event.vibrationValue | number:'1.1-1'}} g</td>
+      <td>{{event.duration}}</td>
+      <td>
+        <button class="btn btn-sm btn-details" (click)="showDetails(event)">
+          详情
+        </button>
+      </td>
+    </tr>
+
+    <!-- 分页控件 -->
+<div class="pagination">
+  第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
+  <button (click)="prevPage()" [disabled]="currentPage === 1">上一页</button>
+  <button (click)="nextPage()" [disabled]="currentPage === totalPages">下一页</button>
+</div>
+
+<!-- 导出按钮 -->
+<button class="btn btn-sm btn-export" (click)="exportCSV()">
+  导出CSV
+</button>
+
       </tbody>
     </table>
     
-    <div class="table-footer">
+    <div class="table-footer" *ngIf="!isLoading && events.length > 0">
       <div class="pagination">
-        <button class="btn small" [disabled]="currentPage === 1">上一页</button>
+        <button class="btn small" 
+                [disabled]="currentPage === 1" 
+                (click)="prevPage()">
+          上一页
+        </button>
         <span>第 {{currentPage}} 页 / 共 {{totalPages}} 页</span>
-        <button class="btn small" [disabled]="currentPage === totalPages">下一页</button>
+        <button class="btn small" 
+                [disabled]="currentPage === totalPages" 
+                (click)="nextPage()">
+          下一页
+        </button>
+      </div>
+      <div class="page-size">
+        <span>每页显示</span>
+        <select [(ngModel)]="pageSize" (change)="onPageSizeChange()">
+          <option value="5">5条</option>
+          <option value="10" selected>10条</option>
+          <option value="20">20条</option>
+          <option value="50">50条</option>
+        </select>
       </div>
     </div>
+    
+    <div class="no-data" *ngIf="!isLoading && events.length === 0">
+      没有找到相关事件记录
+    </div>
   </div>
   
   <footer>

+ 289 - 1
industry-device/industry-web/src/modules/industry/machine/page-vibration-history/page-vibration-history.scss

@@ -15,6 +15,7 @@ $card-bg: rgba(255, 255, 255, 0.1);
   padding: 20px;
   max-width: 1800px;
   margin: 0 auto;
+  font-family: 'Arial', sans-serif;
 }
 
 // 头部样式
@@ -39,6 +40,7 @@ header {
     -webkit-background-clip: text;
     background-clip: text;
     -webkit-text-fill-color: transparent;
+    margin: 0;
   }
 
   .tagline {
@@ -126,6 +128,7 @@ header {
   border: 1px solid rgba(255, 255, 255, 0.05);
   position: relative;
   overflow: hidden;
+  
 
   .card-header {
     display: flex;
@@ -138,6 +141,7 @@ header {
       font-size: 20px;
       font-weight: 600;
       color: #fff;
+      margin: 0;
     }
   }
 }
@@ -146,6 +150,20 @@ header {
 .chart-container {
   height: 400px;
   width: 100%;
+  min-height: 400px;
+  position: relative;
+  
+  // 加载状态
+  &.loading {
+    &:after {
+      content: '加载中...';
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: #999;
+    }
+  }
 }
 
 // 历史控制区域
@@ -156,6 +174,81 @@ header {
   padding: 15px;
   background: rgba(0, 0, 0, 0.2);
   border-radius: 8px;
+  
+  .time-range {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    
+    .time-select {
+      padding: 8px 12px;
+      border-radius: 4px;
+      background: rgba(255, 255, 255, 0.1);
+      color: white;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+    }
+    
+    .custom-range {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      input {
+        padding: 8px;
+        border-radius: 4px;
+        background: rgba(255, 255, 255, 0.1);
+        color: white;
+        border: 1px solid rgba(255, 255, 255, 0.2);
+      }
+    }
+  }
+  
+  .search-filter {
+    display: flex;
+    gap: 10px;
+    
+    input {
+      padding: 8px 12px;
+      border-radius: 4px;
+      background: rgba(255, 255, 255, 0.1);
+      color: white;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+      min-width: 250px;
+    }
+  }
+}
+
+// 按钮样式
+.btn {
+  padding: 8px 16px;
+  border-radius: 4px;
+  background: $primary-color;
+  color: white;
+  border: none;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  
+  &:hover {
+    background: darken($primary-color, 10%);
+  }
+  
+  &.small {
+    padding: 5px 10px;
+    font-size: 12px;
+  }
+  
+  &.secondary {
+    background: rgba(255, 255, 255, 0.1);
+    
+    &:hover {
+      background: rgba(255, 255, 255, 0.2);
+    }
+  }
+  
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
 }
 
 // 表格样式
@@ -164,6 +257,201 @@ header {
   background: rgba(0, 0, 0, 0.2);
   border-radius: 8px;
   overflow: hidden;
+  padding: 20px;
+  position: relative; // 新增:为加载指示器定位做准备
+  min-height: 200px; // 新增:防止内容加载时高度塌缩
+
+  // 表头样式
+  .table-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      color: #fff;
+      font-weight: 600;
+    }
+  }
+
+  // 批量操作区域
+  .batch-actions {
+    display: flex;
+    gap: 10px;
+    margin-bottom: 15px;
+    padding: 12px;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 4px;
+    align-items: center;
+
+    select {
+      padding: 6px 10px;
+      background: rgba(255, 255, 255, 0.1);
+      color: white;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+      border-radius: 4px;
+      min-width: 120px;
+    }
+  }
+
+  // 表格主体
+  .event-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 14px;
+
+    th, td {
+      padding: 12px 15px;
+      text-align: left;
+      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+    }
+
+    th {
+      font-weight: 600;
+      color: #ccc;
+      background: rgba(0, 0, 0, 0.3);
+      white-space: nowrap;
+    }
+
+    tr:hover {
+      background: rgba(255, 255, 255, 0.05);
+    }
+
+    // 事件类型颜色标识
+    .event-type {
+      &.warning { color: #ff9800; } // 振动超标/刀具磨损
+      &.danger { color: #f44336; }  // 严重故障
+      &.success { color: #4caf50; } // 正常停机
+    }
+  }
+
+  // 加载指示器
+  .loading-indicator {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    display: flex;
+    align-items: center;
+    color: #aaa;
+
+    .spinner {
+      width: 20px;
+      height: 20px;
+      border: 3px solid rgba(255, 255, 255, 0.3);
+      border-radius: 50%;
+      border-top-color: #00b6c1;
+      animation: spin 1s linear infinite;
+      margin-right: 10px;
+    }
+  }
+
+  // 无数据提示
+  .no-data {
+    text-align: center;
+    padding: 40px;
+    color: #777;
+    font-size: 16px;
+  }
+
+  // 分页样式
+  .table-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20px;
+    padding-top: 15px;
+    border-top: 1px solid rgba(255, 255, 255, 0.1);
+
+    .pagination {
+      display: flex;
+      align-items: center;
+      gap: 15px;
+      color: #ccc;
+
+      button {
+        min-width: 80px;
+      }
+    }
+
+    .page-size {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      color: #ccc;
+
+      select {
+        padding: 5px 8px;
+        background: rgba(255, 255, 255, 0.1);
+        color: white;
+        border: 1px solid rgba(255, 255, 255, 0.2);
+        border-radius: 4px;
+      }
+    }
+  }
+}
+
+// 旋转动画
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+// 事件标签样式
+.event-tag {
+  display: inline-block;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.1);
+  
+  &.warning {
+    background: rgba($warning-color, 0.2);
+    color: $warning-color;
+  }
+  
+  &.danger {
+    background: rgba($danger-color, 0.2);
+    color: $danger-color;
+  }
+  
+  &.success {
+    background: rgba($success-color, 0.2);
+    color: $success-color;
+  }
+}
+
+// 加载指示器
+.loading-indicator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+  color: #ccc;
+  
+  .spinner {
+    width: 20px;
+    height: 20px;
+    border: 3px solid rgba(255, 255, 255, 0.3);
+    border-radius: 50%;
+    border-top-color: $secondary-color;
+    animation: spin 1s ease-in-out infinite;
+    margin-right: 10px;
+  }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+// 无数据提示
+.no-data {
+  text-align: center;
+  padding: 40px;
+  color: #777;
 }
 
 // 页脚样式
@@ -174,4 +462,4 @@ footer {
   font-size: 14px;
   border-top: 1px solid rgba(255, 255, 255, 0.1);
   margin-top: 40px;
-}
+}

+ 439 - 175
industry-device/industry-web/src/modules/industry/machine/page-vibration-history/page-vibration-history.ts

@@ -1,60 +1,57 @@
-import { Component, AfterViewInit, OnDestroy } from '@angular/core';
+import { Component, AfterViewInit, OnDestroy, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
 import * as echarts from 'echarts';
-import { CommonModule } from '@angular/common';
+import { CommonModule, DatePipe, DecimalPipe } from '@angular/common';
 import { RouterModule } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import Parse from 'parse';
+import { VibrationEvent } from '../../../../shared/interfaces/event.interface';
 
 @Component({
   selector: 'app-page-vibration-history',
   standalone: true,
-  imports: [CommonModule, RouterModule],
+  imports: [CommonModule, RouterModule, FormsModule, DatePipe, DecimalPipe],
   templateUrl: './page-vibration-history.html',
   styleUrls: ['./page-vibration-history.scss']
 })
-export class PageVibrationHistory implements AfterViewInit, OnDestroy {
-  showCustomRange = false;
+export class PageVibrationHistory implements AfterViewInit, OnDestroy, OnInit {
+  // 分页相关
   currentPage = 1;
-  totalPages = 5;
+  totalPages = 1;
+  pageSize = 10;
   
-  events = [
-    {
-      time: '2025-06-15 14:23:45',
-      type: '振动超标',
-      device: 'CNC-01',
-      value: 2.8,
-      duration: '12秒'
-    },
-    {
-      time: '2025-06-15 10:12:33',
-      type: '刀具磨损',
-      device: 'CNC-02',
-      value: 1.2,
-      duration: '持续'
-    },
-    {
-      time: '2025-06-14 18:45:21',
-      type: '正常停机',
-      device: 'CNC-01',
-      value: 0.5,
-      duration: '3分12秒'
-    },
-    {
-      time: '2025-06-14 09:30:15',
-      type: '严重故障',
-      device: 'CNC-03',
-      value: 4.2,
-      duration: '2分45秒'
-    },
-    {
-      time: '2025-06-13 16:20:08',
-      type: '振动超标',
-      device: 'CNC-02',
-      value: 2.1,
-      duration: '8秒'
-    }
-  ];
-
+  // 时间范围筛选
+  timeRange = '7days';
+  showCustomRange = false;
+  startDate = '';
+  endDate = '';
+  
+  // 搜索相关
+  searchQuery = '';
+  
+  // 批量操作相关
+  showBatchActions = false;
+  batchAction = '';
+  selectedEvents: string[] = [];
+  
+  // 数据加载状态
+  isLoading = true;
+  
+  // 事件数据
+  events: VibrationEvent[] = [];
+  
+  // 图表实例
   private trendChart!: echarts.ECharts;
   private eventChart!: echarts.ECharts;
+  private chartsInitialized = false;
+
+  @ViewChild('trendChart') trendChartEl!: ElementRef;
+  @ViewChild('eventChart') eventChartEl!: ElementRef;
+
+  constructor(private cdr: ChangeDetectorRef) {}
+
+  ngOnInit() {
+    this.loadMockEvents();
+  }
 
   ngAfterViewInit() {
     this.initCharts();
@@ -72,152 +69,134 @@ export class PageVibrationHistory implements AfterViewInit, OnDestroy {
     this.eventChart?.resize();
   };
 
-  private initCharts() {
-    this.trendChart = echarts.init(document.getElementById('trend-chart') as HTMLElement);
+    private updateCharts() {
     this.trendChart.setOption(this.getTrendChartOption());
-
-    this.eventChart = echarts.init(document.getElementById('event-chart') as HTMLElement);
     this.eventChart.setOption(this.getEventChartOption());
   }
 
+private initCharts() {
+  setTimeout(() => { // 添加延迟确保DOM就绪
+    try {
+      if (!this.trendChartEl?.nativeElement || !this.eventChartEl?.nativeElement) {
+        console.error('图表容器未找到:', {
+          trend: this.trendChartEl,
+          event: this.eventChartEl
+        });
+        return;
+      }
+
+      this.trendChart = echarts.init(this.trendChartEl.nativeElement);
+      this.eventChart = echarts.init(this.eventChartEl.nativeElement);
+      
+      // 强制设置容器大小
+      this.trendChart.resize();
+      this.eventChart.resize();
+      
+      this.chartsInitialized = true;
+      this.updateCharts(); // 立即更新一次
+      
+    } catch (error) {
+      console.error('图表初始化错误:', error);
+    }
+  }, 100);
+}
+
+
+// 添加加载模拟数据的方法
+  loadMockEvents() {
+    this.isLoading = true;
+    this.cdr.detectChanges(); // 手动触发变更检测
+
+    setTimeout(() => {
+      try {
+        this.events = this.generateMockEvents();
+        this.totalPages = Math.ceil(this.events.length / this.pageSize);
+        
+        if (this.chartsInitialized) {
+          this.updateCharts();
+        }
+      } catch (error) {
+        console.error('加载模拟数据失败:', error);
+      } finally {
+        this.isLoading = false;
+        this.cdr.detectChanges();
+      }
+    }, 0);
+  }
+
+
+    // 获取趋势图配置
   private getTrendChartOption(): echarts.EChartsOption {
     const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
-    const data = hours.map((_, i) => {
-      return 0.5 + Math.sin(i / 3) * 0.8 + Math.random() * 0.3;
-    });
+    const data = hours.map((_, i) => 0.5 + Math.sin(i / 3) * 0.8 + Math.random() * 0.3);
 
     return {
       backgroundColor: 'transparent',
-      grid: {
-        top: 30,
-        right: 30,
-        bottom: 40,
-        left: 50
-      },
-      tooltip: {
-        trigger: 'axis'
-      },
+      grid: { top: 30, right: 30, bottom: 40, left: 50 },
+      tooltip: { trigger: 'axis' },
       xAxis: {
         type: 'category',
         data: hours,
-        axisLine: {
-          lineStyle: {
-            color: '#666'
-          }
-        },
-        axisLabel: {
-          color: '#999',
-          rotate: 45
-        }
+        axisLine: { lineStyle: { color: '#666' } },
+        axisLabel: { color: '#999', rotate: 45 }
       },
       yAxis: {
         type: 'value',
         name: '振动值 (g)',
-        axisLine: {
-          lineStyle: {
-            color: '#666'
-          }
-        },
-        axisLabel: {
-          color: '#999'
-        },
-        splitLine: {
-          lineStyle: {
-            color: 'rgba(255, 255, 255, 0.05)'
-          }
-        }
+        axisLine: { lineStyle: { color: '#666' } },
+        axisLabel: { color: '#999' },
+        splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }
       },
-      series: [
-        {
-          name: '振动值',
-          type: 'line',
-          smooth: true,
-          data: data,
-          lineStyle: {
-            width: 2,
-            color: '#00b6c1'
-          },
-          itemStyle: {
-            color: '#00b6c1'
-          },
-          areaStyle: {
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              { offset: 0, color: 'rgba(0, 182, 193, 0.7)' },
-              { offset: 1, color: 'rgba(0, 182, 193, 0.1)' }
-            ])
-          },
-          markLine: {
-            silent: true,
-            lineStyle: {
-              color: '#ff9800',
-              width: 1,
-              type: 'dashed'
-            },
-            data: [
-              {
-                yAxis: 1.5,
-                label: {
-                  formatter: '警告阈值',
-                  position: 'start'
-                }
-              },
-              {
-                yAxis: 2.0,
-                label: {
-                  formatter: '危险阈值',
-                  position: 'start'
-                },
-                lineStyle: {
-                  color: '#f44336'
-                }
-              }
-            ]
-          }
+      series: [{
+        name: '振动值',
+        type: 'line',
+        smooth: true,
+        data: data,
+        lineStyle: { width: 2, color: '#00b6c1' },
+        itemStyle: { color: '#00b6c1' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(0, 182, 193, 0.7)' },
+            { offset: 1, color: 'rgba(0, 182, 193, 0.1)' }
+          ])
+        },
+        markLine: {
+          silent: true,
+          lineStyle: { color: '#ff9800', width: 1, type: 'dashed' },
+          data: [
+            { yAxis: 1.5, label: { formatter: '警告阈值', position: 'start' } },
+            { yAxis: 2.0, label: { formatter: '危险阈值', position: 'start' }, lineStyle: { color: '#f44336' } }
+          ]
         }
-      ]
+      }]
     };
   }
 
   private getEventChartOption(): echarts.EChartsOption {
+    const eventTypes = ['振动超标', '刀具磨损', '正常停机', '严重故障'];
+    const warningData = eventTypes.map(type => 
+      this.events.filter(e => e.eventType === type && ['振动超标', '刀具磨损'].includes(e.eventType)).length
+    );
+    const dangerData = eventTypes.map(type => 
+      this.events.filter(e => e.eventType === type && e.eventType === '严重故障').length
+    );
+
     return {
       backgroundColor: 'transparent',
-      grid: {
-        top: 30,
-        right: 30,
-        bottom: 40,
-        left: 50
-      },
-      tooltip: {
-        trigger: 'axis'
-      },
+      grid: { top: 30, right: 30, bottom: 40, left: 50 },
+      tooltip: { trigger: 'axis' },
       xAxis: {
         type: 'category',
-        data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
-        axisLine: {
-          lineStyle: {
-            color: '#666'
-          }
-        },
-        axisLabel: {
-          color: '#999'
-        }
+        data: eventTypes,
+        axisLine: { lineStyle: { color: '#666' } },
+        axisLabel: { color: '#999' }
       },
       yAxis: {
         type: 'value',
         name: '事件次数',
-        axisLine: {
-          lineStyle: {
-            color: '#666'
-          }
-        },
-        axisLabel: {
-          color: '#999'
-        },
-        splitLine: {
-          lineStyle: {
-            color: 'rgba(255, 255, 255, 0.05)'
-          }
-        }
+        axisLine: { lineStyle: { color: '#666' } },
+        axisLabel: { color: '#999' },
+        splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }
       },
       series: [
         {
@@ -225,44 +204,329 @@ export class PageVibrationHistory implements AfterViewInit, OnDestroy {
           type: 'bar',
           barGap: 0,
           barWidth: 15,
-          data: [2, 1, 3, 2, 1, 0, 1],
-          itemStyle: {
-            color: '#ff9800'
-          }
+          data: warningData,
+          itemStyle: { color: '#ff9800' }
         },
         {
           name: '严重事件',
           type: 'bar',
           barWidth: 15,
-          data: [0, 1, 0, 1, 0, 0, 0],
-          itemStyle: {
-            color: '#f44336'
-          }
+          data: dangerData,
+          itemStyle: { color: '#f44336' }
         }
       ],
       legend: {
         data: ['警告事件', '严重事件'],
-        textStyle: {
-          color: '#ccc'
-        },
+        textStyle: { color: '#ccc' },
         top: 0
       }
     };
   }
 
-  toggleCustomRange(show: boolean) {
-    this.showCustomRange = show;
+  private generateMockEvents(): VibrationEvent[] {
+    const mockEvents: VibrationEvent[] = [];
+    const eventTypes: ('振动超标' | '刀具磨损' | '正常停机' | '严重故障')[] = 
+      ['振动超标', '刀具磨损', '正常停机', '严重故障'];
+    const deviceIds = ['CNC-001', 'CNC-002', 'CNC-003', 'MILL-001', 'LATHE-001'];
+    
+    const now = new Date();
+    const oneDay = 24 * 60 * 60 * 1000;
+    
+    for (let i = 0; i < 50; i++) {
+      const daysAgo = Math.floor(Math.random() * 90);
+      const hoursAgo = Math.floor(Math.random() * 24);
+      const minutesAgo = Math.floor(Math.random() * 60);
+      
+      const eventTime = new Date(now.getTime() - (daysAgo * oneDay) - (hoursAgo * 60 * 60 * 1000) - (minutesAgo * 60 * 1000));
+      const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
+      const deviceId = deviceIds[Math.floor(Math.random() * deviceIds.length)];
+      
+      let vibrationValue: number;
+      switch(eventType) {
+        case '振动超标': vibrationValue = 1.6 + Math.random() * 0.8; break;
+        case '刀具磨损': vibrationValue = 1.2 + Math.random() * 0.6; break;
+        case '严重故障': vibrationValue = 2.5 + Math.random() * 1.5; break;
+        default: vibrationValue = 0.5 + Math.random() * 0.5;
+      }
+      
+      const durationMinutes = Math.floor(Math.random() * 30);
+      const durationSeconds = Math.floor(Math.random() * 60);
+      const duration = `${durationMinutes}:${durationSeconds.toString().padStart(2, '0')}`;
+      
+      mockEvents.push({
+        id: `mock-${i}`,
+        eventTime,
+        eventType,
+        deviceId,
+        vibrationValue,
+        duration
+      });
+    }
+    
+    return mockEvents.sort((a, b) => b.eventTime.getTime() - a.eventTime.getTime());
+  }
+
+  initializeParse() {
+    Parse.initialize('dev', 'devmk');
+    (Parse as any).serverURL = 'http://dev.fmode.cn:1337/parse';
   }
 
-  prevPage() {
+  async loadEvents() {
+    this.isLoading = true;
+    this.selectedEvents = [];
+    
+    const Event = Parse.Object.extend('VibrationEvent');
+    const query = new Parse.Query(Event);
+    
+    this.applyTimeFilter(query);
+
+    if (this.searchQuery) {
+      query.contains('deviceId', this.searchQuery);
+      query.contains('eventType', this.searchQuery);
+    }
+    
+    query.limit(this.pageSize);
+    query.skip((this.currentPage - 1) * this.pageSize);
+    query.descending('eventTime');
+    
+    try {
+      const results = await query.find();
+      this.events = results.map(item => ({
+        id: item.id,
+        eventTime: item.get('eventTime') as Date,
+        eventType: item.get('eventType') as '振动超标' | '刀具磨损' | '正常停机' | '严重故障',
+        deviceId: item.get('deviceId') as string,
+        vibrationValue: item.get('vibrationValue') as number,
+        duration: item.get('duration') as string,
+        rawData: item
+      }));
+      
+      const count = await query.count();
+      this.totalPages = Math.ceil(count / this.pageSize);
+      this.updateCharts();
+      
+    } catch (error) {
+      console.error('Error loading events:', error);
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  private applyTimeFilter(query: Parse.Query) {
+    const now = new Date();
+    
+    switch (this.timeRange) {
+      case 'today':
+        const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+        query.greaterThanOrEqualTo('eventTime', todayStart);
+        break;
+      case '7days':
+        const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+        query.greaterThanOrEqualTo('eventTime', sevenDaysAgo);
+        break;
+      case '30days':
+        const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+        query.greaterThanOrEqualTo('eventTime', thirtyDaysAgo);
+        break;
+      case '90days':
+        const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
+        query.greaterThanOrEqualTo('eventTime', ninetyDaysAgo);
+        break;
+      case 'custom':
+        if (this.startDate && this.endDate) {
+          query.greaterThanOrEqualTo('eventTime', new Date(this.startDate));
+          query.lessThanOrEqualTo('eventTime', new Date(this.endDate));
+        }
+        break;
+    }
+  }
+
+  onTimeRangeChange() {
+    this.showCustomRange = this.timeRange === 'custom';
+    if (this.timeRange !== 'custom') {
+      this.loadEvents();
+    }
+  }
+
+  applyCustomRange() {
+    if (this.startDate && this.endDate) {
+      this.loadEvents();
+    }
+  }
+
+  searchEvents() {
+    this.currentPage = 1;
+    this.loadEvents();
+  }
+
+  resetSearch() {
+    this.searchQuery = '';
+    this.currentPage = 1;
+    this.loadEvents();
+  }
+
+  async prevPage() {
     if (this.currentPage > 1) {
       this.currentPage--;
+      await this.loadEvents();
     }
   }
 
-  nextPage() {
+  async nextPage() {
     if (this.currentPage < this.totalPages) {
       this.currentPage++;
+      await this.loadEvents();
+    }
+  }
+
+  onPageSizeChange() {
+    this.currentPage = 1;
+    this.loadEvents();
+  }
+
+  exportCSV() {
+    try {
+      const headers = ['时间', '事件类型', '设备', '振动值(g)', '持续时间'];
+      const rows = this.events.map(event => [
+        event.eventTime.toLocaleString(),
+        event.eventType,
+        event.deviceId,
+        event.vibrationValue,
+        event.duration
+      ]);
+      
+      const csvContent = [headers, ...rows].map(row => row.join(',')).join('\n');
+      this.downloadCSV(csvContent, '振动事件记录.csv');
+    } catch (error) {
+      console.error('导出CSV失败:', error);
+    }
+  }
+
+  private downloadCSV(content: string, filename: string) {
+    const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
+    const link = document.createElement('a');
+    const url = URL.createObjectURL(blob);
+    
+    link.setAttribute('href', url);
+    link.setAttribute('download', filename);
+    link.style.visibility = 'hidden';
+    
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  }
+
+  toggleSelectEvent(eventId: string | undefined) {
+    if (!eventId) return;
+    
+    const index = this.selectedEvents.indexOf(eventId);
+    if (index > -1) {
+      this.selectedEvents.splice(index, 1);
+    } else {
+      this.selectedEvents.push(eventId);
+    }
+  }
+
+  isSelected(eventId: string | undefined): boolean {
+    if (!eventId) return false;
+    return this.selectedEvents.includes(eventId);
+  }
+
+  showDetails(event: VibrationEvent) {
+    console.log('事件详情:', event);
+    alert(`事件详情:
+时间: ${event.eventTime}
+类型: ${event.eventType}
+设备: ${event.deviceId}
+振动值: ${event.vibrationValue}g
+持续时间: ${event.duration}`);
+  }
+
+  // 新增的方法 - 执行批量操作
+  executeBatchAction() {
+    if (!this.batchAction || this.selectedEvents.length === 0) {
+      alert('请选择要执行的操作和至少一个事件');
+      return;
+    }
+
+    // 根据不同的批量操作类型执行不同的逻辑
+    switch (this.batchAction) {
+      case 'export':
+        this.exportSelectedEvents();
+        break;
+      case 'delete':
+        this.deleteSelectedEvents();
+        break;
+      case 'mark':
+        this.markSelectedEvents();
+        break;
+      default:
+        console.warn('未知的批量操作类型:', this.batchAction);
+    }
+  }
+
+  private exportSelectedEvents() {
+    const selectedEvents = this.events.filter(event => 
+      event.id && this.selectedEvents.includes(event.id)
+    );
+    
+    if (selectedEvents.length === 0) {
+      alert('没有选中的事件可导出');
+      return;
+    }
+
+    const headers = ['时间', '事件类型', '设备', '振动值(g)', '持续时间'];
+    const rows = selectedEvents.map(event => [
+      event.eventTime.toLocaleString(),
+      event.eventType,
+      event.deviceId,
+      event.vibrationValue,
+      event.duration
+    ]);
+    
+    const csvContent = [headers, ...rows].map(row => row.join(',')).join('\n');
+    this.downloadCSV(csvContent, '选中振动事件.csv');
+  }
+
+  private async deleteSelectedEvents() {
+    if (!confirm(`确定要删除选中的 ${this.selectedEvents.length} 个事件吗?`)) {
+      return;
+    }
+
+    try {
+      const Event = Parse.Object.extend('VibrationEvent');
+      const objectsToDelete = this.selectedEvents.map(id => {
+        const obj = new Event();
+        obj.id = id;
+        return obj;
+      });
+
+      await Parse.Object.destroyAll(objectsToDelete);
+      alert('删除成功');
+      this.loadEvents(); // 刷新列表
+    } catch (error) {
+      console.error('删除事件失败:', error);
+      alert('删除失败,请查看控制台日志');
+    }
+  }
+
+  private markSelectedEvents() {
+    // 这里可以实现标记选中事件的逻辑
+    alert(`已标记 ${this.selectedEvents.length} 个事件`);
+  }
+
+  // 新增的方法 - 全选/取消全选
+  toggleSelectAll(event: Event) {
+    const isChecked = (event.target as HTMLInputElement).checked;
+    
+    if (isChecked) {
+      // 全选当前页的所有事件
+      this.selectedEvents = this.events
+        .filter(event => !!event.id)
+        .map(event => event.id as string);
+    } else {
+      // 取消全选
+      this.selectedEvents = [];
     }
   }
 }

+ 11 - 0
industry-device/industry-web/src/shared/interfaces/event.interface.ts

@@ -0,0 +1,11 @@
+// src/app/shared/models/vibration-event.model.ts
+export interface VibrationEvent {
+  id?: string;
+  eventTime: Date;
+  eventType: '振动超标' | '刀具磨损' | '正常停机' | '严重故障';
+  deviceId: string;
+  vibrationValue: number;
+  duration: string;
+  rawData?: any; // 可选,用于存储原始数据
+}
+//import { VibrationEvent } from '../../../../shared/interfaces/event.interface';

+ 0 - 0
industry-device/industry-web/src/style/variables