实验室测控系统开发组件
这是一个专为实验室设备数据采集与分析设计的可视化测控系统组件。采用工业风格界面设计,提供了丰富的动态数据展示与分析功能,可应用于各类实验室环境中的设备监控和数据处理。
功能特点
- 多设备管理与控制:支持多种测量设备的状态监控和控制管理
- 实时数据可视化:提供动态更新的数据图表,直观展示各传感器数据变化
- 数据分析工具:内置FFT分析、统计分析和趋势分析等常用数据分析功能
- 报警系统:可配置的多参数报警机制,及时响应异常情况
- 灵活配置选项:提供采样率、缓冲区大小等核心参数的自定义配置
- 通知系统:系统事件与报警的集中管理和展示
- 数据导出:支持将采集数据导出为标准格式,便于后续处理
- 工业风格界面:专业的深色工业设计风格,降低视觉疲劳
组件区域说明
组件包含以下主要功能区域:
- 设备状态与控制区:显示连接的实验设备状态,提供基本控制功能
- 数据可视化区:动态图表显示采集数据,支持多种显示模式(实时、历史、相关性)
- 数据分析与配置区:
- 数据分析:提供快速分析工具和结果显示
- 测量设置:采样参数和存储选项配置
- 报警设置:温度和压力等参数的警报阈值设置
- 通知系统:汇总显示系统报警和事件通知
- 状态栏:显示数据采集状态信息(数据点数、采集时间、存储空间)
可定制选项
组件提供多种定制选项,可以通过修改代码来调整:
添加新的传感器/设备
- 在HTML的
device-list
区域添加新的设备项 - 在JavaScript中的事件监听器中为新设备添加控制逻辑
- 在数据结构和模拟函数中添加对应的数据处理逻辑
调整界面设计
- 修改CSS中的颜色变量(
:root
部分)可以改变整体配色 - 调整面板的flex属性可以改变各区域的宽度比例
- 修改媒体查询可以优化不同屏幕尺寸下的显示效果
增强数据分析功能
- 在
performFFTAnalysis
、performStatisticalAnalysis
和performTrendAnalysis
函数中添加实际的数据处理算法 - 扩展分析结果的展示方式,如添加更多的可视化图表
连接实际设备
组件目前使用模拟数据进行演示。要连接实际的实验设备,需要:
- 根据设备API替换
simulateNewData
函数中的数据生成逻辑 - 调整数据采集频率和处理方式,适应实际设备的特性
- 在设备控制函数中实现与实际设备的通信逻辑
图表使用说明
组件默认尝试使用Chart.js来绘制图表。如需启用完整的Chart.js功能:
- 或者修改
initializeCharts
函数,添加动态加载Chart.js的逻辑
如果Chart.js不可用,组件会自动使用内置的SVG简易图表进行数据展示。
浏览器兼容性
组件使用了现代Web技术(Flexbox、CSS变量、ES6+),建议在较新版本的浏览器中使用:
- Chrome 60+
- Firefox 55+
- Edge 16+
- Safari 11+
性能优化
对于大规模数据采集场景,建议:
- 调整更新频率,减少DOM操作次数
- 优化数据存储逻辑,避免内存占用过高
- 考虑使用Web Worker处理数据分析任务,提高响应性
项目结构
效果展示
源码
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 id="lab-monitoring-system">
<!-- 顶部标题栏 -->
<div class="lab-header">
<div class="lab-title">实验室测控系统</div>
<div class="lab-status">
<span class="status-label">系统状态:</span>
<span class="status-value" id="system-status">数据采集中</span>
<div class="status-indicator active"></div>
</div>
</div>
<!-- 主内容区域 -->
<div class="lab-content">
<!-- 左侧:设备状态与控制 -->
<div class="panel device-panel">
<div class="panel-header">
<h3>设备状态与控制</h3>
<div class="panel-tools">
<button class="tool-btn" id="refresh-devices" title="刷新设备状态">
<i class="tool-icon">↻</i>
</button>
</div>
</div>
<div class="panel-body">
<div class="device-list" id="device-list">
<div class="device-item">
<div class="device-header">
<span class="device-name">温度传感器阵列</span>
<span class="device-status online">在线</span>
</div>
<div class="device-controls">
<button class="control-btn" id="temp-start">启动</button>
<button class="control-btn" id="temp-stop">停止</button>
<button class="control-btn" id="temp-calibrate">校准</button>
</div>
<div class="device-parameters">
<div class="parameter">
<span class="param-label">采样率:</span>
<span class="param-value">5Hz</span>
</div>
<div class="parameter">
<span class="param-label">精度:</span>
<span class="param-value">±0.1°C</span>
</div>
<div class="parameter">
<span class="param-label">状态:</span>
<span class="param-value">采集中</span>
</div>
</div>
</div>
<div class="device-item">
<div class="device-header">
<span class="device-name">压力传感器</span>
<span class="device-status online">在线</span>
</div>
<div class="device-controls">
<button class="control-btn" id="pressure-start">启动</button>
<button class="control-btn" id="pressure-stop">停止</button>
<button class="control-btn" id="pressure-calibrate">校准</button>
</div>
<div class="device-parameters">
<div class="parameter">
<span class="param-label">采样率:</span>
<span class="param-value">10Hz</span>
</div>
<div class="parameter">
<span class="param-label">精度:</span>
<span class="param-value">±0.01MPa</span>
</div>
<div class="parameter">
<span class="param-label">状态:</span>
<span class="param-value">采集中</span>
</div>
</div>
</div>
<div class="device-item">
<div class="device-header">
<span class="device-name">光谱分析仪</span>
<span class="device-status offline">离线</span>
</div>
<div class="device-controls">
<button class="control-btn" id="spectral-start" disabled>启动</button>
<button class="control-btn" id="spectral-stop" disabled>停止</button>
<button class="control-btn" id="spectral-calibrate" disabled>校准</button>
</div>
<div class="device-parameters">
<div class="parameter">
<span class="param-label">波长范围:</span>
<span class="param-value">380-780nm</span>
</div>
<div class="parameter">
<span class="param-label">分辨率:</span>
<span class="param-value">1.5nm</span>
</div>
<div class="parameter">
<span class="param-label">状态:</span>
<span class="param-value">未连接</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 中间:数据可视化 -->
<div class="panel visualization-panel">
<div class="panel-header">
<h3>数据可视化</h3>
<div class="panel-tools">
<select id="chart-type">
<option value="realtime">实时图表</option>
<option value="historical">历史数据</option>
<option value="correlation">相关性分析</option>
</select>
<select id="timeframe">
<option value="1m">1分钟</option>
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="1h">1小时</option>
<option value="1d">1天</option>
</select>
<button class="tool-btn" id="export-data" title="导出数据">
<i class="tool-icon">↓</i>
</button>
</div>
</div>
<div class="panel-body">
<div class="chart-container">
<div class="chart-header">
<div class="chart-title" id="chart-title">温度传感器实时数据</div>
<div class="chart-legend">
<div class="legend-item">
<span class="legend-color" style="background-color: #2196F3;"></span>
<span class="legend-text">传感器 1</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: #FF5722;"></span>
<span class="legend-text">传感器 2</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background-color: #4CAF50;"></span>
<span class="legend-text">传感器 3</span>
</div>
</div>
</div>
<div class="chart-wrapper" id="chart-area">
<!-- 图表将通过JavaScript绘制 -->
</div>
<div class="chart-info">
<div class="info-item">
<span class="info-label">最大值:</span>
<span class="info-value" id="max-value">27.8°C</span>
</div>
<div class="info-item">
<span class="info-label">最小值:</span>
<span class="info-value" id="min-value">21.2°C</span>
</div>
<div class="info-item">
<span class="info-label">平均值:</span>
<span class="info-value" id="avg-value">24.5°C</span>
</div>
<div class="info-item">
<span class="info-label">标准差:</span>
<span class="info-value" id="std-value">1.23</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧:数据分析与配置 -->
<div class="panel analysis-panel">
<div class="panel-header">
<h3>数据分析与配置</h3>
<div class="panel-tools">
<button class="tool-btn" id="save-config" title="保存配置">
<i class="tool-icon">✓</i>
</button>
</div>
</div>
<div class="panel-body">
<div class="tabs">
<div class="tab-header">
<div class="tab-btn active" data-tab="analysis">数据分析</div>
<div class="tab-btn" data-tab="settings">测量设置</div>
<div class="tab-btn" data-tab="alarms">报警设置</div>
</div>
<div class="tab-content">
<!-- 数据分析内容 -->
<div class="tab-pane active" id="analysis-tab">
<div class="analysis-tools">
<div class="tool-group">
<div class="tool-header">快速分析</div>
<div class="tool-buttons">
<button class="analysis-btn" id="btn-fft">FFT分析</button>
<button class="analysis-btn" id="btn-statistics">统计分析</button>
<button class="analysis-btn" id="btn-trend">趋势分析</button>
</div>
</div>
<div class="analysis-result" id="analysis-result">
<div class="result-header">分析结果</div>
<div class="result-content">
<p>选择一个分析工具开始数据分析。分析结果将显示在此处。</p>
</div>
</div>
</div>
</div>
<!-- 测量设置内容 -->
<div class="tab-pane" id="settings-tab">
<div class="settings-form">
<div class="setting-group">
<div class="setting-header">采集设置</div>
<div class="setting-item">
<label for="sample-rate">采样率 (Hz)</label>
<input type="number" id="sample-rate" class="setting-input" value="10" min="1" max="1000">
</div>
<div class="setting-item">
<label for="buffer-size">缓冲区大小</label>
<input type="number" id="buffer-size" class="setting-input" value="1000" min="100" max="10000">
</div>
<div class="setting-item">
<label for="averaging">平均次数</label>
<input type="number" id="averaging" class="setting-input" value="5" min="1" max="50">
</div>
</div>
<div class="setting-group">
<div class="setting-header">存储设置</div>
<div class="setting-item">
<label for="auto-save">自动保存</label>
<div class="toggle-switch">
<input type="checkbox" id="auto-save" checked>
<span class="toggle-slider"></span>
</div>
</div>
<div class="setting-item">
<label for="save-interval">保存间隔 (分钟)</label>
<input type="number" id="save-interval" class="setting-input" value="5" min="1" max="60">
</div>
</div>
</div>
</div>
<!-- 报警设置内容 -->
<div class="tab-pane" id="alarms-tab">
<div class="alarm-config">
<div class="alarm-header">温度报警设置</div>
<div class="alarm-item">
<div class="alarm-param">高温报警</div>
<div class="alarm-inputs">
<input type="number" id="temp-high" class="alarm-input" value="30" step="0.1">
<span class="alarm-unit">°C</span>
</div>
<div class="toggle-switch">
<input type="checkbox" id="temp-high-enabled" checked>
<span class="toggle-slider"></span>
</div>
</div>
<div class="alarm-item">
<div class="alarm-param">低温报警</div>
<div class="alarm-inputs">
<input type="number" id="temp-low" class="alarm-input" value="18" step="0.1">
<span class="alarm-unit">°C</span>
</div>
<div class="toggle-switch">
<input type="checkbox" id="temp-low-enabled" checked>
<span class="toggle-slider"></span>
</div>
</div>
<div class="alarm-header">压力报警设置</div>
<div class="alarm-item">
<div class="alarm-param">高压报警</div>
<div class="alarm-inputs">
<input type="number" id="pressure-high" class="alarm-input" value="2.5" step="0.01">
<span class="alarm-unit">MPa</span>
</div>
<div class="toggle-switch">
<input type="checkbox" id="pressure-high-enabled" checked>
<span class="toggle-slider"></span>
</div>
</div>
<div class="alarm-item">
<div class="alarm-param">低压报警</div>
<div class="alarm-inputs">
<input type="number" id="pressure-low" class="alarm-input" value="0.5" step="0.01">
<span class="alarm-unit">MPa</span>
</div>
<div class="toggle-switch">
<input type="checkbox" id="pressure-low-enabled">
<span class="toggle-slider"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="lab-footer">
<div class="notifications">
<div class="notification-btn" id="notification-btn">
<i class="notification-icon">!</i>
<span class="notification-count" id="notification-count">2</span>
</div>
<div class="notification-dropdown" id="notification-dropdown">
<div class="notification-header">
<span>系统通知</span>
<button class="clear-btn" id="clear-notifications">清除所有</button>
</div>
<div class="notification-list" id="notification-list">
<div class="notification-item warning">
<div class="notification-time">08:42:15</div>
<div class="notification-content">温度传感器1数值超出警戒范围 (31.2°C)</div>
</div>
<div class="notification-item info">
<div class="notification-time">08:15:20</div>
<div class="notification-content">自动保存已完成,数据已存储至 lab_data_20250409.csv</div>
</div>
</div>
</div>
</div>
<div class="system-info">
<div class="info-segment">
<span class="info-label">数据点数:</span>
<span class="info-value" id="data-points">12,458</span>
</div>
<div class="info-segment">
<span class="info-label">采集时间:</span>
<span class="info-value" id="acquisition-time">02:15:37</span>
</div>
<div class="info-segment">
<span class="info-label">存储空间:</span>
<span class="info-value" id="storage-usage">45.8MB / 1GB</span>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
styles.css
/* 实验室测控系统 - 工业风格CSS */
:root {
--primary-dark: #1a2639;
--primary-medium: #263852;
--primary-light: #3d5174;
--accent-blue: #0288d1;
--accent-blue-light: #5eb8ff;
--accent-blue-dark: #005b9f;
--accent-green: #4caf50;
--accent-amber: #ffb300;
--accent-red: #e53935;
--text-primary: #ffffff;
--text-secondary: #c5c8d9;
--text-muted: #7a8499;
--border-color: #364761;
--shadow-color: rgba(0, 0, 0, 0.3);
--chart-grid: #2c3e50;
}
/* 基本布局和容器样式 */
#lab-monitoring-system {
font-family: 'Roboto', 'Segoe UI', Arial, sans-serif;
background-color: var(--primary-dark);
color: var(--text-primary);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
min-height: 750px;
border-radius: 5px;
box-shadow: 0 4px 16px var(--shadow-color);
overflow: hidden;
border: 1px solid var(--border-color);
}
/* 顶部标题栏 */
.lab-header {
background-color: var(--primary-medium);
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid var(--border-color);
}
.lab-title {
font-size: 18px;
font-weight: 600;
letter-spacing: 0.5px;
color: var(--text-primary);
text-transform: uppercase;
}
.lab-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-label {
color: var(--text-secondary);
font-size: 14px;
}
.status-value {
font-weight: 500;
color: var(--text-primary);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--accent-blue);
position: relative;
}
.status-indicator.active::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: var(--accent-blue);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
70% { transform: scale(1.5); opacity: 0; }
100% { transform: scale(1.5); opacity: 0; }
}
/* 主内容区域 */
.lab-content {
display: flex;
flex: 1;
overflow: hidden;
padding: 10px;
gap: 10px;
}
/* 面板通用样式 */
.panel {
background-color: var(--primary-medium);
border-radius: 5px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 8px var(--shadow-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 10px 14px;
background-color: var(--primary-light);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
}
.panel-tools {
display: flex;
gap: 8px;
align-items: center;
}
.panel-body {
flex: 1;
padding: 12px;
overflow: auto;
}
/* 设备面板样式 */
.device-panel {
flex: 0 0 280px;
}
.device-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.device-item {
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
overflow: hidden;
}
.device-header {
padding: 10px 12px;
background-color: var(--primary-light);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.device-name {
font-weight: 500;
font-size: 14px;
}
.device-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 500;
}
.device-status.online {
background-color: var(--accent-green);
color: #ffffff;
}
.device-status.offline {
background-color: var(--text-muted);
color: var(--primary-dark);
}
.device-controls {
display: flex;
padding: 8px 12px;
gap: 6px;
border-bottom: 1px solid var(--border-color);
}
.control-btn {
flex: 1;
background-color: var(--primary-light);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 5px 8px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
background-color: var(--accent-blue);
}
.control-btn:disabled {
background-color: var(--primary-medium);
color: var(--text-muted);
cursor: not-allowed;
}
.device-parameters {
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.parameter {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.param-label {
color: var(--text-secondary);
}
.param-value {
font-weight: 500;
}
/* 可视化面板样式 */
.visualization-panel {
flex: 1;
min-width: 400px;
}
.chart-container {
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.chart-header {
padding: 10px 12px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--primary-light);
border-bottom: 1px solid var(--border-color);
}
.chart-title {
font-size: 14px;
font-weight: 500;
}
.chart-legend {
display: flex;
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
.chart-wrapper {
flex: 1;
padding: 10px;
min-height: 350px;
position: relative;
background: linear-gradient(
to bottom,
transparent 9px,
var(--chart-grid) 10px
),
linear-gradient(
to right,
transparent 9px,
var(--chart-grid) 10px
);
background-size: 10% 10%;
background-color: var(--primary-dark);
}
.chart-info {
display: flex;
justify-content: space-between;
padding: 8px 12px;
background-color: var(--primary-medium);
border-top: 1px solid var(--border-color);
}
.info-item {
display: flex;
gap: 5px;
font-size: 12px;
}
.info-label {
color: var(--text-secondary);
}
.info-value {
font-weight: 500;
color: var(--text-primary);
}
/* 分析面板样式 */
.analysis-panel {
flex: 0 0 280px;
}
/* 标签页样式 */
.tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.tab-header {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.tab-btn {
padding: 8px 12px;
font-size: 13px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
color: var(--text-secondary);
}
.tab-btn:hover {
color: var(--text-primary);
}
.tab-btn.active {
color: var(--accent-blue);
border-bottom-color: var(--accent-blue);
}
.tab-content {
flex: 1;
overflow: auto;
}
.tab-pane {
display: none;
padding: 10px 0;
}
.tab-pane.active {
display: block;
}
/* 数据分析标签页 */
.analysis-tools {
display: flex;
flex-direction: column;
gap: 12px;
}
.tool-group {
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
padding: 8px;
}
.tool-header {
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-secondary);
}
.tool-buttons {
display: flex;
gap: 6px;
}
.analysis-btn {
flex: 1;
background-color: var(--primary-light);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 6px 8px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.analysis-btn:hover {
background-color: var(--accent-blue);
}
.analysis-result {
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
overflow: hidden;
}
.result-header {
padding: 8px 10px;
font-size: 13px;
background-color: var(--primary-light);
border-bottom: 1px solid var(--border-color);
font-weight: 500;
}
.result-content {
padding: 8px 10px;
font-size: 12px;
color: var(--text-secondary);
min-height: 80px;
}
/* 测量设置标签页 */
.settings-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.setting-group {
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
padding: 10px;
}
.setting-header {
font-size: 13px;
font-weight: 500;
margin-bottom: 10px;
color: var(--text-secondary);
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-item label {
color: var(--text-primary);
}
.setting-input {
width: 80px;
background-color: var(--primary-medium);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 4px 6px;
font-size: 12px;
}
/* 开关按钮 */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 18px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--primary-light);
transition: .4s;
border-radius: 18px;
border: 1px solid var(--border-color);
}
.toggle-slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: var(--text-secondary);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--accent-blue);
}
input:checked + .toggle-slider:before {
transform: translateX(18px);
background-color: white;
}
/* 报警设置标签页 */
.alarm-config {
display: flex;
flex-direction: column;
gap: 6px;
}
.alarm-header {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
padding: 5px 0;
margin-top: 5px;
}
.alarm-item {
display: flex;
align-items: center;
background-color: var(--primary-dark);
border-radius: 4px;
border: 1px solid var(--border-color);
padding: 8px 10px;
}
.alarm-param {
flex: 0 0 80px;
font-size: 12px;
}
.alarm-inputs {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
}
.alarm-input {
width: 60px;
background-color: var(--primary-medium);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 4px 6px;
font-size: 12px;
}
.alarm-unit {
font-size: 12px;
color: var(--text-secondary);
}
/* 工具按钮 */
.tool-btn {
width: 26px;
height: 26px;
background-color: var(--primary-dark);
color: var(--text-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.tool-btn:hover {
background-color: var(--accent-blue);
color: var(--text-primary);
}
.tool-icon {
font-style: normal;
font-size: 14px;
}
/* 下拉选择框 */
select {
background-color: var(--primary-dark);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 4px 8px;
font-size: 12px;
min-width: 100px;
cursor: pointer;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6"><path fill="%237a8499" d="M0 0l6 6 6-6z"/></svg>');
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 24px;
}
/* 底部状态栏 */
.lab-footer {
background-color: var(--primary-medium);
padding: 10px 16px;
border-top: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.system-info {
display: flex;
gap: 16px;
}
.info-segment {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
}
/* 通知系统 */
.notifications {
position: relative;
}
.notification-btn {
position: relative;
width: 30px;
height: 30px;
background-color: var(--primary-light);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.notification-icon {
font-style: normal;
color: var(--text-primary);
font-weight: bold;
}
.notification-count {
position: absolute;
top: -3px;
right: -3px;
min-width: 16px;
height: 16px;
background-color: var(--accent-red);
color: white;
border-radius: 8px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
.notification-dropdown {
position: absolute;
bottom: 40px;
left: 0;
width: 300px;
background-color: var(--primary-medium);
border-radius: 5px;
border: 1px solid var(--border-color);
box-shadow: 0 4px 12px var(--shadow-color);
overflow: hidden;
z-index: 100;
display: none;
}
.notification-dropdown.show {
display: block;
}
.notification-header {
padding: 10px;
background-color: var(--primary-light);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-header span {
font-size: 13px;
font-weight: 500;
}
.clear-btn {
background-color: transparent;
color: var(--accent-blue);
border: none;
font-size: 12px;
cursor: pointer;
padding: 0;
}
.notification-list {
max-height: 250px;
overflow-y: auto;
}
.notification-item {
padding: 8px 10px;
border-bottom: 1px solid var(--border-color);
font-size: 12px;
}
.notification-item:last-child {
border-bottom: none;
}
.notification-time {
color: var(--text-muted);
margin-bottom: 3px;
}
.notification-item.warning .notification-content {
color: var(--accent-amber);
}
.notification-item.error .notification-content {
color: var(--accent-red);
}
.notification-item.info .notification-content {
color: var(--accent-blue-light);
}
/* 响应式调整 */
@media (max-width: 1200px) {
.lab-content {
flex-direction: column;
}
.device-panel, .analysis-panel {
flex: 0 0 auto;
width: 100%;
}
.visualization-panel {
min-height: 400px;
}
}
script.js
// 实验室测控系统JavaScript
(function() {
// 图表实例和数据
let temperatureChart;
let pressureChart;
const temperatureData = {
timestamps: [],
sensor1: [],
sensor2: [],
sensor3: []
};
const pressureData = {
timestamps: [],
values: []
};
// 初始化函数
function initLabMonitoringSystem() {
console.log("实验室测控系统初始化...");
setupEventListeners();
initializeCharts();
startDataSimulation();
setupTabNavigation();
}
// 设置事件监听
function setupEventListeners() {
// 设备控制按钮
document.getElementById('temp-start').addEventListener('click', () => controlDevice('温度传感器阵列', 'start'));
document.getElementById('temp-stop').addEventListener('click', () => controlDevice('温度传感器阵列', 'stop'));
document.getElementById('temp-calibrate').addEventListener('click', () => calibrateDevice('温度传感器阵列'));
document.getElementById('pressure-start').addEventListener('click', () => controlDevice('压力传感器', 'start'));
document.getElementById('pressure-stop').addEventListener('click', () => controlDevice('压力传感器', 'stop'));
document.getElementById('pressure-calibrate').addEventListener('click', () => calibrateDevice('压力传感器'));
// 刷新设备状态
document.getElementById('refresh-devices').addEventListener('click', refreshDeviceStatus);
// 图表类型和时间范围选择
document.getElementById('chart-type').addEventListener('change', updateChartType);
document.getElementById('timeframe').addEventListener('change', updateTimeframe);
// 导出数据按钮
document.getElementById('export-data').addEventListener('click', exportData);
// 分析按钮
document.getElementById('btn-fft').addEventListener('click', performFFTAnalysis);
document.getElementById('btn-statistics').addEventListener('click', performStatisticalAnalysis);
document.getElementById('btn-trend').addEventListener('click', performTrendAnalysis);
// 保存配置
document.getElementById('save-config').addEventListener('click', saveConfiguration);
// 通知系统
document.getElementById('notification-btn').addEventListener('click', toggleNotifications);
document.getElementById('clear-notifications').addEventListener('click', clearNotifications);
}
// 初始化图表
function initializeCharts() {
// 创建温度图表的Canvas元素
const tempChartCanvas = document.createElement('canvas');
tempChartCanvas.id = 'temperature-chart';
document.getElementById('chart-area').appendChild(tempChartCanvas);
// 如果Chart.js可用,则创建图表
if (typeof Chart !== 'undefined') {
setupChartJS();
} else {
// 如果Chart.js不可用,创建一个简单的模拟图表
createSimulatedChart();
}
}
// 使用Chart.js创建图表
function setupChartJS() {
const ctx = document.getElementById('temperature-chart').getContext('2d');
temperatureChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '传感器 1',
data: [],
borderColor: '#2196F3',
backgroundColor: 'rgba(33, 150, 243, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.3
},
{
label: '传感器 2',
data: [],
borderColor: '#FF5722',
backgroundColor: 'rgba(255, 87, 34, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.3
},
{
label: '传感器 3',
data: [],
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
scales: {
x: {
grid: {
color: 'rgba(200, 200, 200, 0.1)'
},
ticks: {
color: '#a7b6c2'
}
},
y: {
grid: {
color: 'rgba(200, 200, 200, 0.1)'
},
ticks: {
color: '#a7b6c2'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
// 创建模拟图表(当Chart.js不可用时)
function createSimulatedChart() {
const chartArea = document.getElementById('chart-area');
chartArea.innerHTML = `
<div class="simulated-chart">
<svg width="100%" height="300" viewBox="0 0 600 300" preserveAspectRatio="none">
<!-- 图表网格 -->
<g class="grid">
<!-- 水平线 -->
<line x1="0" y1="0" x2="600" y2="0" stroke="#2c3e50" stroke-width="1" />
<line x1="0" y1="60" x2="600" y2="60" stroke="#2c3e50" stroke-width="1" />
<line x1="0" y1="120" x2="600" y2="120" stroke="#2c3e50" stroke-width="1" />
<line x1="0" y1="180" x2="600" y2="180" stroke="#2c3e50" stroke-width="1" />
<line x1="0" y1="240" x2="600" y2="240" stroke="#2c3e50" stroke-width="1" />
<line x1="0" y1="299" x2="600" y2="299" stroke="#2c3e50" stroke-width="1" />
<!-- 垂直线 -->
<line x1="0" y1="0" x2="0" y2="300" stroke="#2c3e50" stroke-width="1" />
<line x1="120" y1="0" x2="120" y2="300" stroke="#2c3e50" stroke-width="1" />
<line x1="240" y1="0" x2="240" y2="300" stroke="#2c3e50" stroke-width="1" />
<line x1="360" y1="0" x2="360" y2="300" stroke="#2c3e50" stroke-width="1" />
<line x1="480" y1="0" x2="480" y2="300" stroke="#2c3e50" stroke-width="1" />
<line x1="599" y1="0" x2="599" y2="300" stroke="#2c3e50" stroke-width="1" />
</g>
<!-- 数据线 - 传感器1 -->
<path id="sensor1-path" stroke="#2196F3" stroke-width="2" fill="none" />
<!-- 数据线 - 传感器2 -->
<path id="sensor2-path" stroke="#FF5722" stroke-width="2" fill="none" />
<!-- 数据线 - 传感器3 -->
<path id="sensor3-path" stroke="#4CAF50" stroke-width="2" fill="none" />
</svg>
</div>
`;
// 初始化模拟数据路径
updateSimulatedChart();
}
// 更新模拟图表
function updateSimulatedChart() {
const generateRandomPath = () => {
let path = `M0,150`;
for (let i = 1; i <= 60; i++) {
const x = i * 10;
const y = 150 + (Math.random() * 100 - 50);
path += ` L${x},${y}`;
}
return path;
};
const sensor1Path = document.getElementById('sensor1-path');
const sensor2Path = document.getElementById('sensor2-path');
const sensor3Path = document.getElementById('sensor3-path');
if (sensor1Path) sensor1Path.setAttribute('d', generateRandomPath());
if (sensor2Path) sensor2Path.setAttribute('d', generateRandomPath());
if (sensor3Path) sensor3Path.setAttribute('d', generateRandomPath());
}
// 开始数据模拟
function startDataSimulation() {
// 初始化一些历史数据点
const now = new Date();
for (let i = 60; i >= 0; i--) {
const time = new Date(now - i * 1000);
const timeStr = time.toLocaleTimeString();
temperatureData.timestamps.push(timeStr);
temperatureData.sensor1.push(22 + Math.random() * 6);
temperatureData.sensor2.push(23 + Math.random() * 5);
temperatureData.sensor3.push(21 + Math.random() * 7);
pressureData.timestamps.push(timeStr);
pressureData.values.push(1.8 + Math.random() * 0.5);
}
updateChart();
// 每秒更新一次数据
setInterval(() => {
simulateNewData();
updateChart();
}, 1000);
// 每30秒随机添加一个通知
setInterval(() => {
if (Math.random() < 0.3) {
addRandomNotification();
}
}, 30000);
// 更新采集时间
setInterval(updateAcquisitionTime, 1000);
}
// 模拟新数据
function simulateNewData() {
const now = new Date();
const timeStr = now.toLocaleTimeString();
// 保留最近60个数据点
if (temperatureData.timestamps.length > 60) {
temperatureData.timestamps.shift();
temperatureData.sensor1.shift();
temperatureData.sensor2.shift();
temperatureData.sensor3.shift();
pressureData.timestamps.shift();
pressureData.values.shift();
}
// 添加新的温度数据点 (模拟一些波动)
const lastTemp1 = temperatureData.sensor1[temperatureData.sensor1.length - 1];
const lastTemp2 = temperatureData.sensor2[temperatureData.sensor2.length - 1];
const lastTemp3 = temperatureData.sensor3[temperatureData.sensor3.length - 1];
const newTemp1 = lastTemp1 + (Math.random() * 0.6 - 0.3);
const newTemp2 = lastTemp2 + (Math.random() * 0.6 - 0.3);
const newTemp3 = lastTemp3 + (Math.random() * 0.6 - 0.3);
temperatureData.timestamps.push(timeStr);
temperatureData.sensor1.push(newTemp1);
temperatureData.sensor2.push(newTemp2);
temperatureData.sensor3.push(newTemp3);
// 添加新的压力数据点
const lastPressure = pressureData.values[pressureData.values.length - 1];
const newPressure = lastPressure + (Math.random() * 0.1 - 0.05);
pressureData.timestamps.push(timeStr);
pressureData.values.push(newPressure);
// 更新统计信息
updateStatistics();
// 检查警报
checkAlarms(newTemp1, newTemp2, newTemp3, newPressure);
// 更新数据点计数
updateDataPointCount();
}
// 更新图表
function updateChart() {
const chartType = document.getElementById('chart-type').value;
if (typeof Chart !== 'undefined' && temperatureChart) {
// 使用Chart.js更新图表
if (chartType === 'realtime') {
temperatureChart.data.labels = temperatureData.timestamps;
temperatureChart.data.datasets[0].data = temperatureData.sensor1;
temperatureChart.data.datasets[1].data = temperatureData.sensor2;
temperatureChart.data.datasets[2].data = temperatureData.sensor3;
} else if (chartType === 'historical') {
// 切换到历史数据视图
// 这里为了演示,仅展示了同一数据的不同时间范围
temperatureChart.data.labels = temperatureData.timestamps;
temperatureChart.data.datasets[0].data = temperatureData.sensor1;
temperatureChart.data.datasets[1].data = temperatureData.sensor2;
temperatureChart.data.datasets[2].data = temperatureData.sensor3;
} else if (chartType === 'correlation') {
// 在实际应用中,这里会显示相关性分析的视图
temperatureChart.data.labels = temperatureData.timestamps;
temperatureChart.data.datasets[0].data = temperatureData.sensor1;
temperatureChart.data.datasets[1].data = temperatureData.sensor2;
temperatureChart.data.datasets[2].data = temperatureData.sensor3;
}
temperatureChart.update();
} else {
// 更新模拟图表
updateSimulatedChart();
}
}
// 更新图表类型
function updateChartType() {
const chartType = document.getElementById('chart-type').value;
const chartTitle = document.getElementById('chart-title');
switch (chartType) {
case 'realtime':
chartTitle.textContent = '温度传感器实时数据';
break;
case 'historical':
chartTitle.textContent = '温度传感器历史数据';
break;
case 'correlation':
chartTitle.textContent = '温度与压力相关性分析';
break;
}
updateChart();
}
// 更新时间范围
function updateTimeframe() {
updateChart();
}
// 更新统计信息
function updateStatistics() {
// 计算所有传感器的统计数据
const allValues = [...temperatureData.sensor1, ...temperatureData.sensor2, ...temperatureData.sensor3];
const max = Math.max(...allValues).toFixed(1);
const min = Math.min(...allValues).toFixed(1);
// 计算平均值
const sum = allValues.reduce((a, b) => a + b, 0);
const avg = (sum / allValues.length).toFixed(1);
// 计算标准差
const variance = allValues.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / allValues.length;
const std = Math.sqrt(variance).toFixed(2);
// 更新DOM
document.getElementById('max-value').textContent = max + '°C';
document.getElementById('min-value').textContent = min + '°C';
document.getElementById('avg-value').textContent = avg + '°C';
document.getElementById('std-value').textContent = std;
}
// 检查警报条件
function checkAlarms(temp1, temp2, temp3, pressure) {
// 获取警报阈值
const tempHighThreshold = parseFloat(document.getElementById('temp-high').value);
const tempLowThreshold = parseFloat(document.getElementById('temp-low').value);
const pressureHighThreshold = parseFloat(document.getElementById('pressure-high').value);
const pressureLowThreshold = parseFloat(document.getElementById('pressure-low').value);
// 检查温度高警报
if (document.getElementById('temp-high-enabled').checked) {
if (temp1 > tempHighThreshold) {
addNotification(`温度传感器1数值超出警戒范围 (${temp1.toFixed(1)}°C)`, 'warning');
} else if (temp2 > tempHighThreshold) {
addNotification(`温度传感器2数值超出警戒范围 (${temp2.toFixed(1)}°C)`, 'warning');
} else if (temp3 > tempHighThreshold) {
addNotification(`温度传感器3数值超出警戒范围 (${temp3.toFixed(1)}°C)`, 'warning');
}
}
// 检查温度低警报
if (document.getElementById('temp-low-enabled').checked) {
if (temp1 < tempLowThreshold) {
addNotification(`温度传感器1数值低于警戒范围 (${temp1.toFixed(1)}°C)`, 'warning');
} else if (temp2 < tempLowThreshold) {
addNotification(`温度传感器2数值低于警戒范围 (${temp2.toFixed(1)}°C)`, 'warning');
} else if (temp3 < tempLowThreshold) {
addNotification(`温度传感器3数值低于警戒范围 (${temp3.toFixed(1)}°C)`, 'warning');
}
}
// 检查压力高警报
if (document.getElementById('pressure-high-enabled').checked) {
if (pressure > pressureHighThreshold) {
addNotification(`压力传感器数值超出警戒范围 (${pressure.toFixed(2)} MPa)`, 'warning');
}
}
// 检查压力低警报
if (document.getElementById('pressure-low-enabled').checked) {
if (pressure < pressureLowThreshold) {
addNotification(`压力传感器数值低于警戒范围 (${pressure.toFixed(2)} MPa)`, 'warning');
}
}
}
// 设备控制
function controlDevice(deviceName, action) {
if (action === 'start') {
addNotification(`${deviceName}已启动`, 'info');
} else if (action === 'stop') {
addNotification(`${deviceName}已停止`, 'info');
}
}
// 校准设备
function calibrateDevice(deviceName) {
addNotification(`${deviceName}校准中...`, 'info');
// 模拟校准过程
setTimeout(() => {
addNotification(`${deviceName}校准完成`, 'info');
}, 3000);
}
// 刷新设备状态
function refreshDeviceStatus() {
const refreshBtn = document.getElementById('refresh-devices');
refreshBtn.classList.add('rotating');
// 模拟刷新延迟
setTimeout(() => {
refreshBtn.classList.remove('rotating');
addNotification('设备状态已更新', 'info');
}, 1000);
}
// 添加CSS动画
function addCSSAnimation() {
const style = document.createElement('style');
style.textContent = `
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.rotating {
animation: rotate 1s linear;
}
`;
document.head.appendChild(style);
}
// 导出数据
function exportData() {
const today = new Date().toISOString().slice(0, 10);
addNotification(`数据已导出至 lab_data_${today}.csv`, 'info');
}
// 执行FFT分析
function performFFTAnalysis() {
const resultContent = document.querySelector('.result-content');
resultContent.innerHTML = '<p>正在执行FFT分析...</p>';
// 模拟分析过程
setTimeout(() => {
resultContent.innerHTML = `
<p>FFT分析结果:</p>
<ul>
<li>主频率分量: 0.05 Hz</li>
<li>谐波成分: 0.1 Hz, 0.15 Hz</li>
<li>信噪比: 24.6 dB</li>
</ul>
<p>此温度数据显示周期性变化,可能与环境控制系统相关。</p>
`;
}, 1500);
}
// 执行统计分析
function performStatisticalAnalysis() {
const resultContent = document.querySelector('.result-content');
resultContent.innerHTML = '<p>正在执行统计分析...</p>';
// 模拟分析过程
setTimeout(() => {
// 获取当前统计数据
const max = document.getElementById('max-value').textContent;
const min = document.getElementById('min-value').textContent;
const avg = document.getElementById('avg-value').textContent;
const std = document.getElementById('std-value').textContent;
resultContent.innerHTML = `
<p>统计分析结果:</p>
<ul>
<li>最大值: ${max}</li>
<li>最小值: ${min}</li>
<li>平均值: ${avg}</li>
<li>标准差: ${std}</li>
<li>峰度: 2.86</li>
<li>偏度: 0.12</li>
</ul>
<p>数据分布接近正态分布,温度变化稳定。</p>
`;
}, 1500);
}
// 执行趋势分析
function performTrendAnalysis() {
const resultContent = document.querySelector('.result-content');
resultContent.innerHTML = '<p>正在执行趋势分析...</p>';
// 模拟分析过程
setTimeout(() => {
resultContent.innerHTML = `
<p>趋势分析结果:</p>
<ul>
<li>线性趋势: 微增 (0.02°C/分钟)</li>
<li>预测30分钟后温度: ${(parseFloat(document.getElementById('avg-value').textContent) + 0.6).toFixed(1)}°C</li>
<li>周期性: 存在10分钟周期</li>
<li>异常点数量: 0</li>
</ul>
<p>温度呈现稳定微增趋势,建议监控制冷系统状态。</p>
`;
}, 1500);
}
// 保存配置
function saveConfiguration() {
addNotification('系统配置已保存', 'info');
}
// 切换通知下拉菜单
function toggleNotifications() {
const dropdown = document.getElementById('notification-dropdown');
dropdown.classList.toggle('show');
}
// 添加通知
function addNotification(message, type = 'info') {
const now = new Date();
const timeStr = now.toLocaleTimeString();
const notificationList = document.getElementById('notification-list');
const notificationItem = document.createElement('div');
notificationItem.className = `notification-item ${type}`;
notificationItem.innerHTML = `
<div class="notification-time">${timeStr}</div>
<div class="notification-content">${message}</div>
`;
notificationList.insertBefore(notificationItem, notificationList.firstChild);
// 更新通知计数
updateNotificationCount();
}
// 添加随机通知
function addRandomNotification() {
const notifications = [
{ message: '系统自检完成,所有模块工作正常', type: 'info' },
{ message: '温度数据波动性增加,建议检查环境稳定性', type: 'warning' },
{ message: '数据备份已完成', type: 'info' },
{ message: '压力传感器需要定期维护', type: 'warning' },
{ message: '网络连接稳定性检查已完成', type: 'info' }
];
const randomNotification = notifications[Math.floor(Math.random() * notifications.length)];
addNotification(randomNotification.message, randomNotification.type);
}
// 清除所有通知
function clearNotifications() {
document.getElementById('notification-list').innerHTML = '';
updateNotificationCount();
addNotification('所有通知已清除', 'info');
}
// 更新通知计数
function updateNotificationCount() {
const count = document.getElementById('notification-list').children.length;
document.getElementById('notification-count').textContent = count;
}
// 设置标签页导航
function setupTabNavigation() {
const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.dataset.tab;
// 更新标签按钮状态
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 更新内容面板
tabPanes.forEach(pane => pane.classList.remove('active'));
document.getElementById(`${targetTab}-tab`).classList.add('active');
});
});
}
// 更新数据点计数
function updateDataPointCount() {
const totalPoints = temperatureData.sensor1.length * 3 + pressureData.values.length;
document.getElementById('data-points').textContent = totalPoints.toLocaleString();
}
// 更新采集时间
function updateAcquisitionTime() {
const element = document.getElementById('acquisition-time');
const currentText = element.textContent;
// 将当前时间文本解析为小时、分钟和秒
const [hours, minutes, seconds] = currentText.split(':').map(Number);
// 计算总秒数并增加1秒
let totalSeconds = hours * 3600 + minutes * 60 + seconds + 1;
// 将总秒数转换回时分秒格式
const newHours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const newMinutes = Math.floor(totalSeconds / 60);
const newSeconds = totalSeconds % 60;
// 更新显示
element.textContent = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}:${String(newSeconds).padStart(2, '0')}`;
}
// 添加CSS动画
addCSSAnimation();
// 当DOM加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLabMonitoringSystem);
} else {
initLabMonitoringSystem();
}
})();