当前位置: 首页 > news >正文

27、设备状态监测与维护管理 (模拟电机振动) - /安全与维护组件/device-condition-monitoring

76个工业组件库示例汇总

设备状态监测与维护管理组件 (模拟)

概述

这是一个交互式的 Web 组件,用于模拟工业设备(特别是电机)的状态监测和远程诊断过程。它提供了一个集成的界面,用于查看设备列表、实时运行数据(温度、振动波形)、执行模拟诊断、查看诊断结果(包括模拟的 FFT 频谱)以及管理维护日志。

请注意:这是一个概念演示组件,所有数据和诊断逻辑均为模拟,并非基于真实的物理模型或复杂的分析算法。

主要功能

  • 设备列表与状态:
    • 展示可监控的设备列表,并用颜色点标示其基本状态(良好/警告/严重/离线)。
    • 支持点击选择设备。
  • 实时数据监控:
    • 显示选定设备的名称、实时状态标签和模拟的温度读数。
    • 通过 Canvas 动态绘制模拟的实时振动波形图 (X轴 和 Y轴)。
    • 显示计算出的实时振动 RMS 值。
  • 远程诊断 (模拟):
    • 提供"运行诊断"功能,模拟数据采集和分析过程(带延迟效果)。
    • 显示诊断状态(运行中/完成)。
    • 展示模拟的诊断结论(如:状态正常、轴承磨损、转子不平衡等)。
    • 显示模拟诊断结果的置信度(百分比和进度条)。
    • 通过 Canvas 绘制模拟的振动频谱图 (FFT),根据预设的故障类型显示特征频率峰值。
  • 维护日志管理 (模拟):
    • 显示选定设备的历史维护日志(按时间倒序)。
    • 允许在诊断完成后,模拟添加一条包含诊断结果的维护记录。
  • 界面与风格:
    • 采用苹果科技工业风格,界面简洁、专业。
    • 三栏响应式布局(设备列表 | 状态与实时数据 | 诊断与维护),适应不同屏幕尺寸。
    • 使用 Canvas 2D API 进行数据可视化。

如何使用

  1. 打开页面: 在浏览器中打开 index.html
  2. 选择设备: 点击左侧设备列表中的一个在线设备。
  3. 查看实时数据: 中间区域将显示该设备的实时状态、温度和动态更新的振动波形图及 RMS 值。
  4. 运行诊断: 点击右侧的"运行诊断"按钮。按钮将暂时禁用,状态显示为"诊断运行中…"。
  5. 查看诊断结果: 诊断完成后(约 2.5 秒后),状态更新为"诊断完成",下方将显示诊断结论、置信度,并绘制模拟的 FFT 频谱图。
  6. 添加维护日志: 诊断完成后,"添加维护记录"按钮将启用。点击此按钮会将本次诊断结果模拟添加到该设备的维护日志列表中。
  7. 切换设备: 选择列表中的其他设备将重复上述过程。选择"离线"设备将显示离线状态,并禁用仿真和诊断功能。

文件结构

/安全与维护组件/device-condition-monitoring/
├── index.html       # 组件的 HTML 结构
├── styles.css       # 组件的 CSS 样式
├── script.js        # 组件的 JavaScript 逻辑(数据模拟、交互、绘图)
└── README.md        # 当前说明文件

重要说明

  • 模拟性质: 所有数据(温度、振动)、状态变化、诊断逻辑和 FFT 图都是基于 script.js 中预设的简单规则和随机性生成的,旨在演示概念,不代表真实世界的复杂性。
  • 无外部依赖: 此组件不依赖任何外部 JavaScript 库(如图表库)。
  • 性能: 对于非常频繁或复杂的 Canvas 绘图,性能可能受限。已使用 requestAnimationFrame 进行优化。

效果展示

在这里插入图片描述

源码

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>设备状态监测与维护管理</title><link rel="stylesheet" href="styles.css">
</head>
<body><div class="monitoring-container"><aside class="device-list-panel"><h2>设备列表</h2><ul id="deviceList" class="device-list"><!-- 设备项将由 JS 动态填充 --><li class="placeholder">加载中...</li></ul></aside><main class="main-content-area"><header class="main-header"><h1 id="selectedDeviceName">选择一个设备</h1><div class="status-indicators"><span id="deviceStatus" class="status-tag status-unknown">未知</span><span id="deviceTemp" class="temp-tag">-- °C</span></div></header><section class="live-data-section"><h2>实时数据</h2><div class="data-visualization"><div class="chart-container"><span class="chart-title">振动波形 (X)</span><div id="vibrationWaveformX" class="waveform-placeholder"><canvas id="waveformCanvasX" class="waveform-canvas"></canvas><span class="placeholder-text">等待数据...</span></div><div class="current-vibration-level">实时振动: <span id="currentVibrationX">--</span> mm/s RMS</div></div><div class="chart-container"><span class="chart-title">振动波形 (Y)</span><div id="vibrationWaveformY" class="waveform-placeholder"><canvas id="waveformCanvasY" class="waveform-canvas"></canvas><span class="placeholder-text">等待数据...</span></div><div class="current-vibration-level">实时振动: <span id="currentVibrationY">--</span> mm/s RMS</div></div></div></section></main><aside class="diagnosis-maintenance-panel"><section class="diagnosis-section"><h2>远程诊断</h2><button id="runDiagnosisBtn" class="action-button" disabled>运行诊断</button><div id="diagnosisStatus" class="diagnosis-status">请选择设备并运行诊断</div><div class="chart-container diagnosis-chart"><span class="chart-title">振动频谱 (FFT)</span><div id="fftSpectrum" class="fft-placeholder"><canvas id="fftCanvas" class="fft-canvas"></canvas><span class="placeholder-text">诊断后显示</span></div></div><div id="diagnosisResult" class="diagnosis-result"><h3>诊断结论:</h3><p id="faultType">--</p><h3>置信度:</h3><div class="confidence-display"><div id="diagnosisConfidenceBar" class="confidence-bar"></div><span id="diagnosisConfidenceValue">-- %</span></div></div></section><section class="maintenance-log-section"><h2>维护日志</h2><div id="maintenanceLogList" class="log-list"><!-- 日志条目将由 JS 动态填充 --><p class="placeholder">暂无维护记录</p></div><button id="addLogEntryBtn" class="action-button secondary-button" disabled>添加维护记录 (模拟)</button></section></aside></div><script src="script.js"></script>
</body>
</html> 

styles.css

:root {--bg-color: #f8f8fa; /* Slightly off-white background */--panel-bg: #ffffff;--border-color: #e0e0e6;--text-primary: #1d1d1f;--text-secondary: #6e6e73;--text-placeholder: #a0a0a5;--accent-blue: #007aff;--accent-blue-hover: #005ecb;--status-good: #34c759;--status-warning: #ff9500;--status-critical: #ff3b30;--status-unknown: #8e8e93;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--medium-border-radius: 6px;--small-border-radius: 4px;--panel-padding: 15px;--section-spacing: 20px;--box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}body {margin: 0;padding: 0; /* Remove default padding */font-family: var(--font-family);background-color: var(--bg-color);color: var(--text-primary);line-height: 1.5;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;height: 100vh; /* Ensure body takes full height */display: flex; /* Use flex for container */justify-content: center;align-items: center;overflow: hidden; /* Prevent body scroll */
}.monitoring-container {display: flex;width: 95%; /* Limit width */max-width: 1400px; /* Max width for larger screens */height: 85vh; /* Limit height */max-height: 700px; /* Absolute max height */background-color: var(--panel-bg);border: 1px solid var(--border-color);border-radius: var(--border-radius);box-shadow: var(--box-shadow);overflow: hidden; /* Prevent container scroll, manage internally */
}/* Layout Panels */
.device-list-panel {flex: 0 0 240px; /* Fixed width */border-right: 1px solid var(--border-color);padding: var(--panel-padding);overflow-y: auto; /* Allow scrolling if list is long */background-color: #f2f2f7; /* Slightly different bg */
}.main-content-area {flex: 1 1 auto; /* Flexible width */display: flex;flex-direction: column;padding: var(--panel-padding);overflow: hidden; /* Prevent this area from scrolling, manage internal sections */
}.diagnosis-maintenance-panel {flex: 0 0 320px; /* Fixed width */border-left: 1px solid var(--border-color);padding: var(--panel-padding);display: flex;flex-direction: column;overflow-y: auto; /* Allow scrolling for content */
}/* Typography & Common Elements */
h2 {font-size: 1.1rem;font-weight: 600;color: var(--text-primary);margin-top: 0;margin-bottom: 15px;border-bottom: 1px solid var(--border-color);padding-bottom: 8px;
}h3 {font-size: 0.9rem;font-weight: 600;color: var(--text-primary);margin-top: 15px;margin-bottom: 5px;
}.placeholder,
.placeholder-text {color: var(--text-placeholder);font-style: italic;font-size: 0.9rem;text-align: center;padding: 10px 0;
}.action-button {display: block;width: 100%;padding: 8px 15px;font-size: 0.9rem;font-weight: 500;color: #fff;background-color: var(--accent-blue);border: none;border-radius: var(--medium-border-radius);cursor: pointer;transition: background-color 0.2s ease;text-align: center;margin-bottom: 15px;
}.action-button:hover:not(:disabled) {background-color: var(--accent-blue-hover);
}.action-button:disabled {background-color: #c7c7cc; /* Disabled color */cursor: not-allowed;opacity: 0.7;
}.action-button.secondary-button {background-color: #e5e5ea;color: var(--accent-blue);
}.action-button.secondary-button:hover:not(:disabled) {background-color: #dcdce0;
}/* Device List Panel */
.device-list {list-style: none;padding: 0;margin: 0;
}.device-list-item {padding: 10px 8px;margin-bottom: 5px;border-radius: var(--small-border-radius);cursor: pointer;transition: background-color 0.15s ease;display: flex;justify-content: space-between;align-items: center;
}.device-list-item:hover {background-color: #e9e9ed;
}.device-list-item.active {background-color: var(--accent-blue);color: #fff;
}.device-list-item.active .status-dot {background-color: #fff; /* Make dot white on active blue bg */
}.device-name {font-weight: 500;font-size: 0.95rem;
}.status-dot {width: 8px;height: 8px;border-radius: 50%;display: inline-block;margin-left: 8px;flex-shrink: 0;
}/* Main Content Area */
.main-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: var(--section-spacing);
}.main-header h1 {margin: 0;font-size: 1.4rem;font-weight: 600;
}.status-indicators {display: flex;gap: 10px;align-items: center;
}.status-tag {padding: 3px 8px;border-radius: var(--small-border-radius);font-size: 0.8rem;font-weight: 500;color: #fff;
}.temp-tag {padding: 3px 8px;border-radius: var(--small-border-radius);font-size: 0.8rem;font-weight: 500;background-color: #e5e5ea;color: var(--text-secondary);
}/* Status Colors */
.status-good, .status-dot.good { background-color: var(--status-good); }
.status-warning, .status-dot.warning { background-color: var(--status-warning); }
.status-critical, .status-dot.critical { background-color: var(--status-critical); }
.status-offline, .status-dot.offline { background-color: var(--status-unknown); }
.status-unknown, .status-dot.unknown { background-color: var(--status-unknown); }.live-data-section {flex-grow: 1; /* Takes remaining space */overflow: hidden; /* Prevents section scroll */display: flex;flex-direction: column;
}.live-data-section h2 {margin-bottom: 10px;border-bottom: none;padding-bottom: 0;
}.data-visualization {flex-grow: 1;display: flex;gap: var(--panel-padding);overflow: hidden;
}.chart-container {flex: 1;background-color: #fdfdff;border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);padding: 10px;display: flex;flex-direction: column;position: relative; /* For absolute positioning of placeholder text */
}.chart-title {font-size: 0.8rem;font-weight: 500;color: var(--text-secondary);margin-bottom: 5px;text-align: center;
}.waveform-placeholder,
.fft-placeholder {flex-grow: 1;display: flex;align-items: center;justify-content: center;background-color: #f8f8fa;border: 1px dashed var(--border-color);border-radius: var(--small-border-radius);position: relative; /* Needed for canvas overlay */min-height: 100px; /* Ensure minimum height */
}.waveform-placeholder .placeholder-text,
.fft-placeholder .placeholder-text {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 1;
}.waveform-canvas, .fft-canvas {display: block;width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: 2; /* Ensure canvas is above placeholder bg */
}.current-vibration-level {font-size: 0.85rem;color: var(--text-secondary);text-align: center;margin-top: 8px;
}/* Diagnosis & Maintenance Panel */
.diagnosis-section,
.maintenance-log-section {margin-bottom: var(--section-spacing);
}.diagnosis-status {font-size: 0.9rem;color: var(--text-secondary);margin-bottom: 15px;padding: 8px;background-color: #f2f2f7;border-radius: var(--small-border-radius);text-align: center;
}.diagnosis-result {margin-top: 15px;
}.diagnosis-result h3 {margin-top: 10px;margin-bottom: 5px;
}.diagnosis-result p {margin: 0 0 10px 0;font-size: 0.9rem;color: var(--text-primary);
}.confidence-display {display: flex;align-items: center;gap: 8px;margin-bottom: 10px;
}.confidence-bar {flex-grow: 1;height: 8px;background-color: #e5e5ea;border-radius: 4px;overflow: hidden;
}#diagnosisConfidenceBar {background-color: var(--accent-blue); /* Progress color */height: 100%;width: 0%; /* Will be set by JS */transition: width 0.3s ease-out;
}#diagnosisConfidenceValue {font-size: 0.9rem;font-weight: 500;color: var(--text-secondary);min-width: 40px; /* Prevent layout shift */text-align: right;
}.log-list {max-height: 200px; /* Limit log height */overflow-y: auto;border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);padding: 10px;background-color: #fdfdff;margin-bottom: 10px;
}.log-entry {font-size: 0.85rem;margin-bottom: 8px;padding-bottom: 8px;border-bottom: 1px dashed #eee;
}.log-entry:last-child {margin-bottom: 0;padding-bottom: 0;border-bottom: none;
}.log-timestamp {font-weight: 500;color: var(--text-secondary);display: block;margin-bottom: 3px;
}.log-details {color: var(--text-primary);
}/* Responsive Design */
@media (max-width: 900px) {.monitoring-container {flex-direction: column;height: auto; /* Allow content height */max-height: none;width: 100%;border: none;border-radius: 0;box-shadow: none;}.device-list-panel,.diagnosis-maintenance-panel {flex: 0 0 auto; /* Reset flex basis */width: 100%;border-right: none;border-left: none;border-bottom: 1px solid var(--border-color);max-height: 250px; /* Limit height on mobile */overflow-y: auto;}.main-content-area {order: -1; /* Move main content to top on mobile if needed, or keep order */padding-bottom: 0;}.data-visualization {flex-direction: column;}
} 

script.js

document.addEventListener('DOMContentLoaded', () => {// --- DOM Elements ---const deviceListElement = document.getElementById('deviceList');const selectedDeviceNameElement = document.getElementById('selectedDeviceName');const deviceStatusElement = document.getElementById('deviceStatus');const deviceTempElement = document.getElementById('deviceTemp');const currentVibrationXElement = document.getElementById('currentVibrationX');const currentVibrationYElement = document.getElementById('currentVibrationY');const waveformCanvasX = document.getElementById('waveformCanvasX');const waveformCanvasY = document.getElementById('waveformCanvasY');const fftCanvas = document.getElementById('fftCanvas');const runDiagnosisBtn = document.getElementById('runDiagnosisBtn');const diagnosisStatusElement = document.getElementById('diagnosisStatus');const diagnosisResultElement = document.getElementById('diagnosisResult');const faultTypeElement = document.getElementById('faultType');const diagnosisConfidenceBarElement = document.getElementById('diagnosisConfidenceBar');const diagnosisConfidenceValueElement = document.getElementById('diagnosisConfidenceValue');const maintenanceLogListElement = document.getElementById('maintenanceLogList');const addLogEntryBtn = document.getElementById('addLogEntryBtn');const waveformPlaceholderX = document.getElementById('vibrationWaveformX');const waveformPlaceholderY = document.getElementById('vibrationWaveformY');const fftPlaceholder = document.getElementById('fftSpectrum');// Canvas Contextsconst waveformCtxX = waveformCanvasX.getContext('2d');const waveformCtxY = waveformCanvasY.getContext('2d');const fftCtx = fftCanvas.getContext('2d');// --- State ---let devices = {}; // Will be populated with device datalet selectedDeviceId = null;let simulationInterval = null;let lastDiagnosisResult = null; // Store the result for logginglet animationFrameIdX = null;let animationFrameIdY = null;let animationFrameIdFFT = null;let currentVibrationDataX = [];let currentVibrationDataY = [];const WAVEFORM_POINTS = 256; // Number of points for waveform display// --- Configuration ---const SIMULATION_INTERVAL_MS = 100; // Update data every 100msconst DIAGNOSIS_DURATION_MS = 2500; // Simulate diagnosis time// --- Sample Device Data & Simulation Logic ---const sampleDevices = {"motor-001": { name: "主冷却泵电机 #1", status: "good", baseTemp: 45, tempFluctuation: 2, vibrationAmp: 0.5, faultType: "none", maintenanceLog: [] },"motor-002": { name: "进料传送带电机 #A", status: "warning", baseTemp: 55, tempFluctuation: 4, vibrationAmp: 1.5, faultType: "unbalance", faultFreq: 30, faultAmp: 0.8, maintenanceLog: [{ timestamp: new Date(Date.now() - 86400000 * 2).toLocaleString(), details: "检测到轻微不平衡,已记录观察。" }] },"motor-003": { name: "排风机电机 #3", status: "critical", baseTemp: 70, tempFluctuation: 6, vibrationAmp: 2.5, faultType: "bearing_wear", faultFreq: 120, faultAmp: 1.5, maintenanceLog: [{ timestamp: new Date(Date.now() - 86400000 * 5).toLocaleString(), details: "振动超标,建议检查轴承。" }, { timestamp: new Date(Date.now() - 86400000 * 1).toLocaleString(), details: "轴承磨损加剧,计划停机更换。" }] },"motor-004": { name: "搅拌器电机 #B", status: "offline", baseTemp: 25, tempFluctuation: 1, vibrationAmp: 0.1, faultType: "none", maintenanceLog: [{ timestamp: new Date(Date.now() - 86400000 * 7).toLocaleString(), details: "设备离线维护。" }] }};function generateVibrationData(baseAmp, faultType, faultFreq, faultAmp, numPoints) {const data = [];const timeStep = 1 / (numPoints * 5); // Adjust frequency range simulationfor (let i = 0; i < numPoints; i++) {let noise = (Math.random() - 0.5) * baseAmp * 0.5; // Base noiselet signal = noise;if (faultType !== "none" && faultAmp > 0 && faultFreq > 0) {// Simulate fault frequency componentsignal += Math.sin(2 * Math.PI * faultFreq * i * timeStep) * faultAmp;}// Add some general lower frequency components for realismsignal += Math.sin(2 * Math.PI * 10 * i * timeStep) * baseAmp * 0.2;signal += Math.sin(2 * Math.PI * 50 * i * timeStep) * baseAmp * 0.1;data.push(signal);}return data;}function calculateRMS(data) {if (!data || data.length === 0) return 0;let sumOfSquares = data.reduce((sum, value) => sum + value * value, 0);return Math.sqrt(sumOfSquares / data.length);}// --- UI Update Functions ---function populateDeviceList() {deviceListElement.innerHTML = ''; // Clear placeholder or existing listdevices = JSON.parse(JSON.stringify(sampleDevices)); // Deep copy sample dataObject.keys(devices).forEach(id => {const device = devices[id];const listItem = document.createElement('li');listItem.className = 'device-list-item';listItem.dataset.deviceId = id;const nameSpan = document.createElement('span');nameSpan.className = 'device-name';nameSpan.textContent = device.name;const statusDot = document.createElement('span');statusDot.className = `status-dot ${device.status}`; // Use device status for colorlistItem.appendChild(nameSpan);listItem.appendChild(statusDot);listItem.addEventListener('click', () => selectDevice(id));deviceListElement.appendChild(listItem);});}function updateStatusIndicators(device) {if (!device) return;deviceStatusElement.textContent = {good: "运行良好",warning: "警告",critical: "严重",offline: "离线",unknown: "未知"}[device.status] || "未知";deviceStatusElement.className = `status-tag status-${device.status}`;deviceTempElement.textContent = `${device.currentTemp.toFixed(1)} °C`;// Update status dot in the list as wellconst listItem = deviceListElement.querySelector(`[data-device-id="${selectedDeviceId}"] .status-dot`);if (listItem) {listItem.className = `status-dot ${device.status}`;}}function updateRealtimeVibrationDisplay(rmsX, rmsY) {currentVibrationXElement.textContent = rmsX.toFixed(2);currentVibrationYElement.textContent = rmsY.toFixed(2);}function resetDiagnosisUI() {diagnosisStatusElement.textContent = '请选择设备并运行诊断';faultTypeElement.textContent = '--';diagnosisConfidenceBarElement.style.width = '0%';diagnosisConfidenceValueElement.textContent = '-- %';runDiagnosisBtn.disabled = !selectedDeviceId || devices[selectedDeviceId]?.status === 'offline';addLogEntryBtn.disabled = true; // Disable until diagnosis is runlastDiagnosisResult = null;clearCanvas(fftCtx, fftCanvas, fftPlaceholder); // Clear FFT canvas}function updateMaintenanceLog(deviceId) {maintenanceLogListElement.innerHTML = ''; // Clear existing logsconst device = devices[deviceId];if (device && device.maintenanceLog && device.maintenanceLog.length > 0) {// Sort logs newest firstdevice.maintenanceLog.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));device.maintenanceLog.forEach(log => {const logEntry = document.createElement('div');logEntry.className = 'log-entry';logEntry.innerHTML = `<span class="log-timestamp">${log.timestamp}</span><p class="log-details">${log.details}</p>`;maintenanceLogListElement.appendChild(logEntry);});} else {maintenanceLogListElement.innerHTML = '<p class="placeholder">暂无维护记录</p>';}}// --- Canvas Drawing Functions ---function clearCanvas(ctx, canvas, placeholderDiv) {ctx.clearRect(0, 0, canvas.width, canvas.height);if (placeholderDiv) {placeholderDiv.querySelector('.placeholder-text').style.display = 'block'; // Show placeholder}// Cancel any pending animation framesif (canvas.id === 'waveformCanvasX') cancelAnimationFrame(animationFrameIdX);if (canvas.id === 'waveformCanvasY') cancelAnimationFrame(animationFrameIdY);if (canvas.id === 'fftCanvas') cancelAnimationFrame(animationFrameIdFFT);}function setupCanvas(canvas, placeholderDiv) {const dpr = window.devicePixelRatio || 1;const rect = placeholderDiv.getBoundingClientRect();// Check if dimensions are validif (rect.width <= 0 || rect.height <= 0) {console.warn(`Canvas container ${placeholderDiv.id} has zero dimensions. Cannot setup canvas.`);return false; // Indicate failure}canvas.width = rect.width * dpr;canvas.height = rect.height * dpr;canvas.style.width = `${rect.width}px`;canvas.style.height = `${rect.height}px`;const ctx = canvas.getContext('2d');ctx.scale(dpr, dpr);placeholderDiv.querySelector('.placeholder-text').style.display = 'none'; // Hide placeholderreturn true; // Indicate success}function drawWaveform(ctx, canvas, placeholderDiv, data) {if (!setupCanvas(canvas, placeholderDiv)) return; // Ensure canvas is readyconst width = canvas.width / (window.devicePixelRatio || 1);const height = canvas.height / (window.devicePixelRatio || 1);const centerY = height / 2;const stepX = width / (data.length - 1);const maxAbsValue = data.reduce((max, val) => Math.max(max, Math.abs(val)), 1); // Avoid division by zeroconst scaleY = height / 2 / maxAbsValue * 0.8; // 80% vertical scalectx.clearRect(0, 0, width, height); // Use scaled width/heightctx.strokeStyle = 'var(--accent-blue)';ctx.lineWidth = 1.5;ctx.beginPath();ctx.moveTo(0, centerY - data[0] * scaleY);for (let i = 1; i < data.length; i++) {ctx.lineTo(i * stepX, centerY - data[i] * scaleY);}ctx.stroke();// Assign animation frame ID based on canvas// if (canvas.id === 'waveformCanvasX') animationFrameIdX = requestAnimationFrame(() => drawWaveform(ctx, canvas, placeholderDiv, data)); // Continuous redraw if needed// if (canvas.id === 'waveformCanvasY') animationFrameIdY = requestAnimationFrame(() => drawWaveform(ctx, canvas, placeholderDiv, data));}// Simplified FFT drawing - just draws peaks based on fault typefunction drawFFT(ctx, canvas, placeholderDiv, device) {if (!setupCanvas(canvas, placeholderDiv)) return; // Ensure canvas is readyconst width = canvas.width / (window.devicePixelRatio || 1);const height = canvas.height / (window.devicePixelRatio || 1);const maxFreqDisplay = 200; // Hz - Simulate display rangeconst baseLineY = height - 10; // Bottom linectx.clearRect(0, 0, width, height);ctx.strokeStyle = 'var(--text-secondary)';ctx.lineWidth = 0.5;// Draw baselinectx.beginPath();ctx.moveTo(0, baseLineY);ctx.lineTo(width, baseLineY);ctx.stroke();// Draw simulated peaksctx.fillStyle = 'var(--accent-blue)';const drawPeak = (freq, amplitude) => {if (freq <= 0 || amplitude <= 0) return;const x = (freq / maxFreqDisplay) * width;const peakHeight = Math.min(amplitude * (height - 20) / 3, height - 20); // Scale amplitude, max heightif (x < width && x > 0) {ctx.fillRect(x - 1.5, baseLineY - peakHeight, 3, peakHeight); // Draw a thin bar}};// Base noise level (low broad peak) - simulate general vibrationdrawPeak(10, device.vibrationAmp * 0.3); // Low freq componentdrawPeak(50, device.vibrationAmp * 0.2); // Another low freq component// Draw specific fault peak if presentif (device.faultType !== "none" && device.faultFreq > 0 && device.faultAmp > 0) {drawPeak(device.faultFreq, device.faultAmp * 1.5); // Make fault peak prominent// Maybe add harmonics for bearing wear?if (device.faultType === "bearing_wear") {drawPeak(device.faultFreq * 2, device.faultAmp * 0.7);drawPeak(device.faultFreq * 3, device.faultAmp * 0.5);}}// Assign animation frame ID// animationFrameIdFFT = requestAnimationFrame(() => drawFFT(ctx, canvas, placeholderDiv, device)); // Continuous redraw if needed}// --- Simulation Control ---function startSimulation(deviceId) {stopSimulation(); // Clear any existing intervalconst device = devices[deviceId];if (!device || device.status === 'offline') {resetUIForDevice(deviceId); // Show offline state correctlyreturn;}simulationInterval = setInterval(() => {// Simulate temp fluctuationdevice.currentTemp = device.baseTemp + (Math.random() - 0.5) * device.tempFluctuation;// Simulate vibration datacurrentVibrationDataX = generateVibrationData(device.vibrationAmp, device.faultType, device.faultFreq, device.faultAmp, WAVEFORM_POINTS);// Simulate slightly different data for Y axiscurrentVibrationDataY = generateVibrationData(device.vibrationAmp * 0.8, device.faultType, device.faultFreq, device.faultAmp * 0.7, WAVEFORM_POINTS);const rmsX = calculateRMS(currentVibrationDataX);const rmsY = calculateRMS(currentVibrationDataY);// Simple status update based on RMS (example thresholds)if (rmsX > 2.0 || rmsY > 2.0) device.status = "critical";else if (rmsX > 1.0 || rmsY > 1.0) device.status = "warning";else device.status = "good";// Update UIupdateStatusIndicators(device);updateRealtimeVibrationDisplay(rmsX, rmsY);// Use requestAnimationFrame for smoother drawinganimationFrameIdX = requestAnimationFrame(() => drawWaveform(waveformCtxX, waveformCanvasX, waveformPlaceholderX, currentVibrationDataX));animationFrameIdY = requestAnimationFrame(() => drawWaveform(waveformCtxY, waveformCanvasY, waveformPlaceholderY, currentVibrationDataY));}, SIMULATION_INTERVAL_MS);}function stopSimulation() {if (simulationInterval) {clearInterval(simulationInterval);simulationInterval = null;}// Cancel any pending animation frames immediatelycancelAnimationFrame(animationFrameIdX);cancelAnimationFrame(animationFrameIdY);cancelAnimationFrame(animationFrameIdFFT);}function resetUIForDevice(deviceId) {stopSimulation();const device = devices[deviceId];selectedDeviceNameElement.textContent = device ? device.name : "选择一个设备";if (device && device.status !== 'offline') {// Set default values before simulation startsdevice.currentTemp = device.baseTemp; // Start with base tempcurrentVibrationDataX = Array(WAVEFORM_POINTS).fill(0); // Zero data initiallycurrentVibrationDataY = Array(WAVEFORM_POINTS).fill(0);updateStatusIndicators(device);updateRealtimeVibrationDisplay(0, 0); // Show zero RMSdrawWaveform(waveformCtxX, waveformCanvasX, waveformPlaceholderX, currentVibrationDataX);drawWaveform(waveformCtxY, waveformCanvasY, waveformPlaceholderY, currentVibrationDataY);} else {// Handle offline or no device selectedselectedDeviceNameElement.textContent = device ? device.name : "选择一个设备";deviceStatusElement.textContent = device ? "离线" : "未知";deviceStatusElement.className = `status-tag status-${device ? 'offline' : 'unknown'}`;deviceTempElement.textContent = "-- °C";currentVibrationXElement.textContent = "--";currentVibrationYElement.textContent = "--";clearCanvas(waveformCtxX, waveformCanvasX, waveformPlaceholderX);clearCanvas(waveformCtxY, waveformCanvasY, waveformPlaceholderY);}resetDiagnosisUI();updateMaintenanceLog(deviceId);}// --- Event Handlers ---function selectDevice(deviceId) {if (selectedDeviceId === deviceId) return; // No change// Update active class in listif (selectedDeviceId) {deviceListElement.querySelector(`[data-device-id="${selectedDeviceId}"]`)?.classList.remove('active');}deviceListElement.querySelector(`[data-device-id="${deviceId}"]`)?.classList.add('active');selectedDeviceId = deviceId;const device = devices[selectedDeviceId];resetUIForDevice(deviceId); // Reset UI firstif (device && device.status !== 'offline') {runDiagnosisBtn.disabled = false;startSimulation(deviceId); // Start simulation for the selected device} else {runDiagnosisBtn.disabled = true;// Explicitly clear canvases if offline/invalidclearCanvas(waveformCtxX, waveformCanvasX, waveformPlaceholderX);clearCanvas(waveformCtxY, waveformCanvasY, waveformPlaceholderY);clearCanvas(fftCtx, fftCanvas, fftPlaceholder);}addLogEntryBtn.disabled = true; // Disable log until diagnosislastDiagnosisResult = null; // Clear previous diagnosis}function handleRunDiagnosis() {if (!selectedDeviceId) return;const device = devices[selectedDeviceId];if (!device || device.status === 'offline') return;runDiagnosisBtn.disabled = true;addLogEntryBtn.disabled = true;diagnosisStatusElement.textContent = '诊断运行中...';clearCanvas(fftCtx, fftCanvas, fftPlaceholder); // Clear previous FFT// Simulate diagnosis delaysetTimeout(() => {let fault = "状态正常";let confidence = Math.random() * 10 + 90; // High confidence for normal// Simplified diagnosis based on simulated stateconst rmsX = calculateRMS(currentVibrationDataX);const rmsY = calculateRMS(currentVibrationDataY);const maxRMS = Math.max(rmsX, rmsY);if (device.faultType === "bearing_wear" && maxRMS > 1.8) {fault = "轴承磨损 (疑似)";confidence = Math.random() * 15 + 80; // 80-95% confidence} else if (device.faultType === "unbalance" && maxRMS > 0.8) {fault = "转子不平衡 (轻微)";confidence = Math.random() * 10 + 85; // 85-95% confidence} else if (maxRMS > 1.0) { // General high vibrationfault = "振动偏高 (原因待查)";confidence = Math.random() * 20 + 70; // 70-90% confidence}diagnosisStatusElement.textContent = '诊断完成';faultTypeElement.textContent = fault;diagnosisConfidenceValueElement.textContent = `${confidence.toFixed(1)} %`;diagnosisConfidenceBarElement.style.width = `${confidence}%`;// Draw the simulated FFT based on the *current* device state used for diagnosisanimationFrameIdFFT = requestAnimationFrame(() => drawFFT(fftCtx, fftCanvas, fftPlaceholder, device));runDiagnosisBtn.disabled = false;addLogEntryBtn.disabled = false; // Enable logging after diagnosis// Store result for logginglastDiagnosisResult = { fault, confidence };}, DIAGNOSIS_DURATION_MS);}function handleAddLogEntry() {if (!selectedDeviceId || !lastDiagnosisResult) return; // Need a device and a diagnosisconst device = devices[selectedDeviceId];const now = new Date();const timestamp = now.toLocaleString();let details = `执行远程诊断。结果:${lastDiagnosisResult.fault} (置信度: ${lastDiagnosisResult.confidence.toFixed(1)}%)。`;// Add a mock maintenance actionif (lastDiagnosisResult.fault !== "状态正常") {details += " 建议:根据诊断结果安排检查。";} else {details += " 建议:继续监控。";}const newLog = { timestamp, details };// Add to the beginning of the log array for the specific deviceif (!device.maintenanceLog) {device.maintenanceLog = [];}device.maintenanceLog.unshift(newLog);updateMaintenanceLog(selectedDeviceId); // Refresh the log displayaddLogEntryBtn.disabled = true; // Disable after adding one log entry (per diagnosis)lastDiagnosisResult = null; // Clear diagnosis result after logging}// --- Initialization ---populateDeviceList();resetDiagnosisUI(); // Initial reset// Add event listenersrunDiagnosisBtn.addEventListener('click', handleRunDiagnosis);addLogEntryBtn.addEventListener('click', handleAddLogEntry);// Add resize listener to redraw canvaseswindow.addEventListener('resize', () => {if (selectedDeviceId) {const device = devices[selectedDeviceId];// Redraw waveformsif (device && device.status !== 'offline') {if (currentVibrationDataX.length > 0) {requestAnimationFrame(() => drawWaveform(waveformCtxX, waveformCanvasX, waveformPlaceholderX, currentVibrationDataX));}if (currentVibrationDataY.length > 0) {requestAnimationFrame(() => drawWaveform(waveformCtxY, waveformCanvasY, waveformPlaceholderY, currentVibrationDataY));}// Redraw FFT if diagnosis was runif (fftPlaceholder.querySelector('.placeholder-text').style.display === 'none') {requestAnimationFrame(() => drawFFT(fftCtx, fftCanvas, fftPlaceholder, device));}} else {clearCanvas(waveformCtxX, waveformCanvasX, waveformPlaceholderX);clearCanvas(waveformCtxY, waveformCanvasY, waveformPlaceholderY);clearCanvas(fftCtx, fftCanvas, fftPlaceholder);}} else {clearCanvas(waveformCtxX, waveformCanvasX, waveformPlaceholderX);clearCanvas(waveformCtxY, waveformCanvasY, waveformPlaceholderY);clearCanvas(fftCtx, fftCanvas, fftPlaceholder);}});// Initial setup for placeholder messagesclearCanvas(waveformCtxX, waveformCanvasX, waveformPlaceholderX);clearCanvas(waveformCtxY, waveformCanvasY, waveformPlaceholderY);clearCanvas(fftCtx, fftCanvas, fftPlaceholder);updateMaintenanceLog(null); // Show initial placeholder for logs}); 
http://www.xdnf.cn/news/1340569.html

相关文章:

  • 【用户管理】修改文件权限
  • DeepSeek V3.1正式发布,专为下代国产芯设计
  • opencv学习:图像边缘检测
  • 8.21IPSEC安全基础后篇,IKE工作过程
  • 基于Matlab的饮料满瓶检测图像处理
  • 面试压力测试破解:如何从容应对棘手问题与挑战
  • 火语言 RPA 进阶功能:让自动化更实用​
  • 利用DeepSeek编写调用系统命令用正则表达式替换文件中文本的程序
  • vmware安装centos7
  • 大数据毕业设计选题推荐-基于大数据的鲍鱼多重生理特征数据可视化分析系统-Spark-Hadoop-Bigdata
  • 代码随想录算法训练营27天 | ​​56. 合并区间、738.单调递增的数字、968.监控二叉树(提高)
  • 嵌入式-中断的概念,优先级,编程-Day17
  • 亚马逊站外推广效能重构:自然排名提升的逻辑与实操边界
  • 底层逻辑颠覆者:Agentic BI如何通过“Data + AI Agent”架构重构数据价值链?
  • Trae AI 超级团队
  • matplotlib 6 - Gallery Images
  • 力扣905:按奇偶排序数组
  • 【GPT入门】第52课 openwebui安装与使用
  • postman接口自动化测试
  • redis在Spring中应用相关
  • Django ModelForm
  • C#基础编程核心知识点总结
  • 打破传统课程模式,IP变现的创新玩法 | 创客匠人
  • RabbitMQ面试精讲 Day 26:RabbitMQ监控体系建设
  • 从零开始的Agent学习(二)-增加文档输出功能
  • 36、供应链计划与执行优化 (军方后勤) - /供应链管理组件/military-logistics-scm
  • 34、扩展仓储管理系统 (跨境汽车零部件模拟) - /物流与仓储组件/extended-warehouse-management
  • 3D 环形旋转图片轮播(纯html,css,js)
  • 力扣hot100:无重复字符的最长子串,找到字符串中所有字母异位词(滑动窗口算法讲解)(3,438)
  • 从零开始理解 K 均值聚类:原理、实现与应用