18、时序数据库 (TSDB) 存储高密度传感器数据组件 - /数据与物联网组件/tsdb-power-plant-archive

76个工业组件库示例汇总

时序数据库 (TSDB) 存储高密度传感器数据组件 (模拟)

概述

这是一个交互式的 Web 组件,用于模拟从时序数据库 (Time Series Database, TSDB) 查询和展示发电厂高密度温度传感器历史数据的场景。用户可以选择不同的温度测点,定义一个时间范围,然后模拟执行查询,并查看结果的统计摘要、数据样本以及一个预留的趋势图区域。

请注意:这是一个前端模拟演示,它不连接任何真实的数据库,所有数据和查询延迟都是在浏览器端模拟生成的。

主要功能

  • 模拟 TSDB 查询: 模拟针对高密度传感器数据的历史查询过程。
  • 传感器选择: 提供多个预定义的发电厂温度测点供用户选择。
  • 时间范围定义: 允许用户通过日期时间选择器指定查询的开始和结束时间。
  • 查询执行与状态反馈:
    • 点击按钮触发模拟查询。
    • 显示查询状态(查询中、完成、无数据、失败)和加载指示器。
    • 模拟网络和数据库处理的延迟。
  • 结果展示:
    • 统计摘要: 显示查询时间范围内数据的最小值、最大值、平均值和数据点总数。
    • 数据样本: 以表格形式展示少量(首尾)查询到的数据点(时间戳和数值)。
    • 趋势图占位符: 预留了用于展示温度变化趋势图的区域(当前未实现具体图表绘制)。
  • 模拟数据归档: 右上角计数器持续增加,模拟传感器数据不断被写入 TSDB。
  • 界面风格: 采用苹果科技风格,布局简洁,响应式设计。

如何使用

  1. 打开页面: 在浏览器中打开 index.html
  2. 查看默认状态: 页面加载后,会显示默认选中的测点和过去一小时的时间范围。
  3. 选择测点: 从"选择温度测点"下拉菜单中选择你感兴趣的传感器。
  4. 定义时间范围: 使用日期时间选择器设置查询的"开始时间"和"结束时间"。确保开始时间早于结束时间,否则"查询"按钮将不可用。
  5. 执行查询: 点击"查询历史数据"按钮。
  6. 观察结果:
    • 查询状态会更新,并显示加载动画。
    • 模拟延迟后,结果区域会显示所选测点的统计摘要和数据样本。
    • 如果时间范围过短或模拟逻辑未生成数据,会提示"无数据"。

模拟细节

  • 数据生成: 查询时,脚本会根据所选测点的预设参数(基础值、波动幅度、噪声)和时间范围,通过叠加正弦波(模拟日周期和小时周期)和随机噪声来动态生成模拟温度数据。每个测点的参数不同,以产生不同的数据模式。
  • 归档计数: 右上角的"已归档数据点"是一个纯粹的视觉模拟,表示数据在持续写入,其增长速度与实际查询生成的数据量无关。
  • 查询延迟: 查询按钮按下后,会有一个 0.5 到 2 秒的随机延迟,用以模拟真实查询所需的时间。
  • 数据密度: 模拟生成数据时,尝试模拟约每 5 秒一个数据点,但查询返回的总点数上限为 5000 个,以避免浏览器性能问题。

文件结构

数据与物联网组件/tsdb-power-plant-archive/
├── index.html         # 组件的 HTML 结构
├── styles.css         # 组件的 CSS 样式
├── script.js          # 组件的 JavaScript 逻辑(模拟查询、数据生成、交互)
└── README.md          # 本说明文件

技术栈

  • HTML5
  • CSS3 (使用了 CSS 变量, Flexbox/Grid 布局)
  • JavaScript (原生 JS, 无外部库依赖)

效果展示

在这里插入图片描述

源码

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>TSDB - 发电厂历史温度归档</title>
    <link rel="stylesheet" href="styles.css">
    <!-- Placeholder for potential charting library -->
</head>
<body>
    <div class="tsdb-container">
        <header class="main-header">
            <h1>时序数据库 - 发电厂温度数据归档模拟</h1>
            <div class="header-info">
                <span title="Simulated number of data points archived since page load">模拟已归档点数: <span id="archivedPointsCounter">0</span></span>
                <span>时间: <span id="currentTime">--:--:--</span></span>
            </div>
        </header>

        <main class="main-content">
            <!-- Column 1: Sensor Selection & Query Params -->
            <section class="query-params-section">
                <h2><i class="icon icon-tag"></i> 查询参数</h2>

                <div class="param-group">
                    <label for="sensorSelect"><i class="icon icon-sensor"></i> 选择温度测点:</label>
                    <select id="sensorSelect">
                        <option value="" disabled selected>-- 请选择测点 --</option>
                        <!-- Options populated by JS -->
                    </select>
                </div>

                <div class="param-group">
                    <label><i class="icon icon-time"></i> 选择时间范围:</label>
                    <div class="time-inputs">
                        <div>
                            <label for="startTime">开始时间:</label>
                            <input type="datetime-local" id="startTime" name="startTime">
                        </div>
                        <div>
                            <label for="endTime">结束时间:</label>
                            <input type="datetime-local" id="endTime" name="endTime">
                        </div>
                    </div>
                </div>

                <div class="param-group action-group">
                    <button id="queryButton" class="action-button" disabled><i class="icon icon-query"></i> 查询历史数据</button>
                    <div class="query-status">
                        状态: <span id="queryStatus" class="status-idle">空闲</span>
                        <span id="querySpinner" class="spinner" style="display: none;"></span>
                    </div>
                </div>

            </section>

            <!-- Column 2: Query Results -->
            <section class="results-section">
                <h2><i class="icon icon-results"></i> 查询结果 (<span id="selectedSensorDisplay">未选择测点</span>)</h2>

                <div id="resultsPlaceholder" class="placeholder-results">
                    请选择测点和时间范围,然后点击查询。
                </div>

                <div id="resultsContent" style="display: none;">
                    <div class="summary-stats">
                        <h3><i class="icon icon-stats"></i> 统计摘要</h3>
                        <div class="stats-grid">
                            <div class="stat-item"><span>最低温度 (°C):</span> <strong id="statMinTemp">--</strong></div>
                            <div class="stat-item"><span>最高温度 (°C):</span> <strong id="statMaxTemp">--</strong></div>
                            <div class="stat-item"><span>平均温度 (°C):</span> <strong id="statAvgTemp">--</strong></div>
                            <div class="stat-item"><span>模拟点数:</span> <strong id="statPointCount">--</strong></div>
                        </div>
                    </div>

                    <div class="sample-data">
                        <h3><i class="icon icon-table"></i> 数据抽样 (模拟)</h3>
                        <div class="table-container">
                            <table>
                                <thead>
                                    <tr>
                                        <th>时间戳</th>
                                        <th>温度值 (°C)</th>
                                    </tr>
                                </thead>
                                <tbody id="sampleDataTableBody">
                                    <!-- Sample rows populated by JS -->
                                    <tr><td colspan="2" class="placeholder">暂无数据</td></tr>
                                </tbody>
                            </table>
                        </div>
                    </div>

                    <div class="trend-chart">
                        <h3><i class="icon icon-chart"></i> 温度趋势 (模拟)</h3>
                        <div class="chart-placeholder" id="tempTrendChart">
                            <p>温度变化趋势图</p>
                            <div class="fake-chart tsdb-trend"></div>
                        </div>
                    </div>
                </div>
            </section>
        </main>

        <footer class="main-footer">
             <p>&copy; 2024 模拟 TSDB 查询系统. 概念演示.</p>
        </footer>
    </div>

    <script src="script.js"></script>
</body>
</html> 

styles.css

:root {
    --background-color: #f0f2f5;
    --container-bg: #ffffff;
    --header-bg: #6c757d; /* Neutral grey header */
    --header-text: #ffffff;
    --section-border: #dee2e6;
    --input-bg: #ffffff;
    --input-border: #ced4da;
    --input-focus-border: #86b7fe;
    --input-focus-shadow: rgba(13, 110, 253, 0.25);
    --button-bg: #0d6efd;
    --button-hover-bg: #0b5ed7;
    --button-disabled-bg: #adb5bd;
    --text-primary: #212529;
    --text-secondary: #6c757d;
    --text-label: #495057;
    --value-color: #000000;
    --status-idle: var(--text-secondary);
    --status-querying: #ffc107; /* Yellow */
    --status-done: #198754; /* Green */
    --status-error: #dc3545; /* Red */
    --placeholder-bg: #f8f9fa;
    --table-header-bg: #f1f3f5;
    --table-row-hover-bg: #e9ecef;
    --chart-placeholder-bg: #f8f9fa;
    --spinner-color: var(--button-bg);
    --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --border-radius: 6px;
    --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.07);
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: var(--font-family);
    background-color: var(--background-color);
    color: var(--text-primary);
    line-height: 1.5;
    overflow-x: hidden;
}

.tsdb-container {
    max-width: 1400px;
    margin: 1rem auto;
    background-color: var(--container-bg);
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    min-height: calc(100vh - 2rem);
    max-height: calc(100vh - 2rem);
    height: 750px; /* Adjust height as needed */
}

/* Header */
.main-header {
    background-color: var(--header-bg);
    color: var(--header-text);
    padding: 0.7rem 1.5rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;
    border-bottom: 1px solid rgba(0,0,0,0.1);
}

.main-header h1 {
    font-size: 1.25rem;
    font-weight: 500;
}

.header-info span {
    margin-left: 1.5rem;
    font-size: 0.85rem;
    opacity: 0.9;
}

/* Main Content Layout */
.main-content {
    flex-grow: 1;
    padding: 1rem;
    overflow: hidden;
    display: grid;
    grid-template-columns: 400px 1fr; /* Fixed width for params, flexible results */
    gap: 1rem;
}

/* Query Parameters Section */
.query-params-section {
    background-color: #fdfdfd;
    border: 1px solid var(--section-border);
    border-radius: var(--border-radius);
    padding: 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    overflow-y: auto;
}

.query-params-section h2,
.results-section h2 {
    font-size: 1.15rem;
    font-weight: 500;
    margin-bottom: 0.5rem;
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--section-border);
    display: flex;
    align-items: center;
    color: var(--text-primary);
    flex-shrink: 0;
}

.query-params-section h2 .icon,
.results-section h2 .icon {
    margin-right: 0.6rem;
    font-size: 1.05em;
}

.param-group {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.param-group > label:first-child {
    font-weight: 500;
    color: var(--text-label);
    font-size: 0.9rem;
    display: flex;
    align-items: center;
}

.param-group > label:first-child .icon {
     margin-right: 0.4rem;
}

select,
input[type="datetime-local"] {
    width: 100%;
    padding: 0.5rem 0.75rem;
    font-size: 0.9rem;
    border: 1px solid var(--input-border);
    border-radius: var(--border-radius);
    background-color: var(--input-bg);
    transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}

select:focus,
input[type="datetime-local"]:focus {
    border-color: var(--input-focus-border);
    outline: 0;
    box-shadow: 0 0 0 0.2rem var(--input-focus-shadow);
}

.time-inputs {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding-left: 1.5rem; /* Indent time inputs */
}

.time-inputs div {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.time-inputs label {
    font-size: 0.8rem;
    color: var(--text-secondary);
}

.action-group {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px dashed var(--section-border);
    align-items: center;
    flex-direction: row;
    justify-content: space-between;
}

.action-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    background-color: var(--button-bg);
    color: white;
    border: none;
    padding: 0.6rem 1.2rem;
    border-radius: var(--border-radius);
    cursor: pointer;
    font-size: 0.95rem;
    font-weight: 500;
    transition: background-color 0.2s ease;
    flex-shrink: 0;
}

.action-button:hover:not(:disabled) {
    background-color: var(--button-hover-bg);
}

.action-button:disabled {
    background-color: var(--button-disabled-bg);
    cursor: not-allowed;
}

.query-status {
    font-size: 0.85rem;
    color: var(--text-secondary);
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

#queryStatus {
    font-weight: 500;
}
#queryStatus.status-idle { color: var(--status-idle); }
#queryStatus.status-querying { color: var(--status-querying); }
#queryStatus.status-done { color: var(--status-done); }
#queryStatus.status-error { color: var(--status-error); }

/* Spinner */
.spinner {
    border: 3px solid rgba(0, 0, 0, 0.1);
    border-left-color: var(--spinner-color);
    border-radius: 50%;
    width: 16px;
    height: 16px;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* Results Section */
.results-section {
    background-color: #fdfdfd;
    border: 1px solid var(--section-border);
    border-radius: var(--border-radius);
    padding: 1.2rem;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.results-section h2 #selectedSensorDisplay {
    font-weight: normal;
    color: var(--text-secondary);
    font-size: 0.9em;
    margin-left: 0.3rem;
}

.placeholder-results {
    flex-grow: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    font-style: italic;
    text-align: center;
    background-color: var(--placeholder-bg);
    border-radius: var(--border-radius);
}

#resultsContent {
    display: flex; /* Changed from none initially */
    flex-direction: column;
    gap: 1.5rem;
    /* flex-grow: 1; */
}

.results-section h3 {
    font-size: 1rem;
    font-weight: 500;
    margin-bottom: 0.8rem;
    display: flex;
    align-items: center;
    color: var(--text-label);
}

.results-section h3 .icon {
    margin-right: 0.5rem;
    font-size: 1em;
}

/* Summary Stats */
.summary-stats {
    background-color: var(--widget-bg);
    padding: 1rem;
    border-radius: var(--border-radius);
    border: 1px solid var(--widget-border);
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 0.5rem 1rem;
}

.stat-item {
    font-size: 0.9rem;
}

.stat-item span {
    color: var(--text-secondary);
    margin-right: 0.4rem;
}

.stat-item strong {
    font-weight: 600;
    color: var(--value-color);
}

/* Sample Data Table */
.sample-data {
     background-color: var(--widget-bg);
    padding: 1rem;
    border-radius: var(--border-radius);
    border: 1px solid var(--widget-border);
}

.table-container {
    max-height: 200px; /* Limit table height */
    overflow-y: auto;
    border: 1px solid var(--section-border);
    border-radius: var(--border-radius);
}

table {
    width: 100%;
    border-collapse: collapse;
}

thead th {
    background-color: var(--table-header-bg);
    padding: 0.6rem 0.8rem;
    text-align: left;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--section-border);
    position: sticky; /* Keep header visible */
    top: 0;
}

tbody td {
    padding: 0.5rem 0.8rem;
    font-size: 0.85rem;
    border-bottom: 1px solid var(--section-border);
}

tbody tr:last-child td {
    border-bottom: none;
}

tbody tr:hover {
    background-color: var(--table-row-hover-bg);
}

/* Trend Chart */
.trend-chart {
     background-color: var(--widget-bg);
    padding: 1rem;
    border-radius: var(--border-radius);
    border: 1px solid var(--widget-border);
}

.chart-placeholder {
    background-color: var(--chart-placeholder-bg);
    border-radius: var(--border-radius);
    padding: 1rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--widget-border);
    min-height: 150px;
    position: relative;
}

.chart-placeholder p {
    font-size: 0.85rem;
    color: var(--text-secondary);
    margin-bottom: 0.5rem;
    font-weight: 500;
}

.fake-chart.tsdb-trend {
    width: 95%;
    height: 70%;
    border: 1px dashed var(--text-secondary);
    position: relative;
    overflow: hidden;
}

.fake-chart.tsdb-trend::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(to right, transparent, rgba(220, 53, 69, 0.1), rgba(255, 193, 7, 0.15), rgba(13, 110, 253, 0.1), transparent);
    animation: fakeTrendPulse 8s ease-in-out infinite;
}

@keyframes fakeTrendPulse {
    0% { transform: scaleY(0.5) translateY(30%); opacity: 0.4; }
    50% { transform: scaleY(1) translateY(0); opacity: 0.7; }
    100% { transform: scaleY(0.5) translateY(30%); opacity: 0.4; }
}

/* Footer */
.main-footer {
    background-color: #f8f8f8;
    padding: 0.5rem 1.5rem;
    text-align: center;
    font-size: 0.8rem;
    color: var(--text-secondary);
    border-top: 1px solid var(--section-border);
    flex-shrink: 0;
}

/* Responsive Design */
@media (max-width: 992px) {
    .main-content {
        grid-template-columns: 1fr; /* Stack columns */
        grid-template-rows: auto 1fr;
    }
    .query-params-section { grid-row: 1; }
    .results-section { grid-row: 2; }

     .tsdb-container {
        max-height: none;
        height: auto;
        margin: 0.5rem;
    }
    .stats-grid {
         grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    }
}

@media (max-width: 768px) {
    .main-content {
        padding: 0.8rem;
        gap: 0.8rem;
    }
    .query-params-section, .results-section {
        padding: 1rem;
    }
     .action-group {
        flex-direction: column;
        align-items: stretch;
        gap: 0.8rem;
    }
    .query-status {
         justify-content: center;
         margin-top: 0.5rem;
    }
    .main-header {
        padding: 0.6rem 1rem;
    }
     .main-header h1 {
        font-size: 1.1rem;
    }
}

/* Basic Icon Placeholders */
.icon::before {
    display: inline-block;
    font-weight: normal;
    font-style: normal;
    font-variant: normal;
    text-rendering: auto;
    -webkit-font-smoothing: antialiased;
    margin-right: 0.3em;
}
.icon-tag::before { content: "🏷️"; }
.icon-sensor::before { content: "🌡️"; } /* Thermometer */
.icon-time::before { content: "⏱️"; }
.icon-query::before { content: "🔍"; }
.icon-results::before { content: "📊"; }
.icon-stats::before { content: "🔢"; }
.icon-table::before { content: "📋"; }
.icon-chart::before { content: "📈"; } 

script.js

// script.js - TSDB Power Plant Archive Component

document.addEventListener('DOMContentLoaded', () => {
    // --- DOM Elements ---
    const currentTimeEl = document.getElementById('currentTime');
    const archivedPointsCounterEl = document.getElementById('archivedPointsCounter');
    const sensorSelect = document.getElementById('sensorSelect');
    const startTimeInput = document.getElementById('startTime');
    const endTimeInput = document.getElementById('endTime');
    const queryButton = document.getElementById('queryButton');
    const queryStatusEl = document.getElementById('queryStatus');
    const querySpinnerEl = document.getElementById('querySpinner');
    const selectedSensorDisplayEl = document.getElementById('selectedSensorDisplay');
    const resultsPlaceholderEl = document.getElementById('resultsPlaceholder');
    const resultsContentEl = document.getElementById('resultsContent');
    const statMinTempEl = document.getElementById('statMinTemp');
    const statMaxTempEl = document.getElementById('statMaxTemp');
    const statAvgTempEl = document.getElementById('statAvgTemp');
    const statPointCountEl = document.getElementById('statPointCount');
    const sampleDataTableBody = document.getElementById('sampleDataTableBody');
    // Chart placeholder elements are not directly manipulated in this simulation

    // --- Simulation State & Parameters ---
    let simulationTime = new Date();
    let archivedPoints = 0;
    let isQuerying = false;
    const sensors = {
        'Boiler_ZoneA_Temp': { name: '锅炉区域A温度', base: 600, amplitude: 25, noise: 5 },
        'Boiler_ZoneB_Temp': { name: '锅炉区域B温度', base: 650, amplitude: 30, noise: 6 },
        'Turbine_Inlet_Temp': { name: '汽轮机入口温度', base: 550, amplitude: 15, noise: 3 },
        'Condenser_Outlet_Temp': { name: '冷凝器出口温度', base: 45, amplitude: 5, noise: 1 },
        'Feedwater_Heater_Temp': { name: '给水加热器温度', base: 180, amplitude: 10, noise: 2 }
    };

    // Update intervals
    const timeUpdateInterval = 1000;
    const archiveInterval = 100; // Simulate points archiving quickly

    // --- Initialization ---
    function initializeTSDBMonitor() {
        populateSensorSelect();
        setDefaultTimeRange();
        setupEventListeners();

        updateTime();
        setInterval(updateTime, timeUpdateInterval);

        // Start simulated archiving
        setInterval(() => {
            archivedPoints += Math.floor(Math.random() * 5 + 1); // Archive 1-5 points per interval
            archivedPointsCounterEl.textContent = archivedPoints.toLocaleString();
        }, archiveInterval);

        validateInputs(); // Initial validation check
        console.log("TSDB Monitor Initialized");
    }

    function populateSensorSelect() {
        for (const tag in sensors) {
            const option = document.createElement('option');
            option.value = tag;
            option.textContent = `${sensors[tag].name} (${tag})`;
            sensorSelect.appendChild(option);
        }
    }

    function setDefaultTimeRange() {
        const now = new Date();
        const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);

        // Format for datetime-local input (YYYY-MM-DDTHH:mm)
        endTimeInput.value = formatDateForInput(now);
        startTimeInput.value = formatDateForInput(oneHourAgo);
    }

    function formatDateForInput(date) {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        return `${year}-${month}-${day}T${hours}:${minutes}`;
    }

    // --- Event Handlers ---
    function setupEventListeners() {
        sensorSelect.addEventListener('change', validateInputs);
        startTimeInput.addEventListener('change', validateInputs);
        endTimeInput.addEventListener('change', validateInputs);
        queryButton.addEventListener('click', handleQuery);
    }

    function validateInputs() {
        const sensorSelected = sensorSelect.value !== '';
        const startTimeValid = startTimeInput.value !== '';
        const endTimeValid = endTimeInput.value !== '';
        let timeRangeValid = false;
        if (startTimeValid && endTimeValid) {
            const start = new Date(startTimeInput.value);
            const end = new Date(endTimeInput.value);
            timeRangeValid = start < end;
        }

        queryButton.disabled = !(sensorSelected && timeRangeValid);
        return sensorSelected && timeRangeValid;
    }

    function handleQuery() {
        if (isQuerying || !validateInputs()) {
            return;
        }

        isQuerying = true;
        setQueryStatus('querying', '查询中...');
        resultsPlaceholderEl.style.display = 'none'; // Hide placeholder immediately
        resultsContentEl.style.display = 'none'; // Hide old results
        sampleDataTableBody.innerHTML = '<tr><td colspan="2" class="placeholder">生成数据中...</td></tr>'; // Clear table

        const selectedTag = sensorSelect.value;
        const sensorInfo = sensors[selectedTag];
        selectedSensorDisplayEl.textContent = sensorInfo ? sensorInfo.name : '未知测点';

        // Simulate network/DB delay
        const queryDelay = Math.random() * 1500 + 500; // 0.5s to 2s delay
        setTimeout(() => {
            try {
                const startTime = new Date(startTimeInput.value);
                const endTime = new Date(endTimeInput.value);
                const simulatedData = generateSimulatedData(sensorInfo, startTime, endTime);

                if (simulatedData.length === 0) {
                     setQueryStatus('error', '无数据');
                     resultsPlaceholderEl.textContent = `在选定时间范围 (${startTime.toLocaleString()} - ${endTime.toLocaleString()}) 内,${sensorInfo.name} 无模拟数据。`;
                     resultsPlaceholderEl.style.display = 'block';
                     resultsContentEl.style.display = 'none';
                } else {
                    processAndRenderResults(simulatedData);
                    setQueryStatus('done', '完成');
                }
            } catch (error) {
                console.error("Query simulation error:", error);
                setQueryStatus('error', '查询失败');
                 resultsPlaceholderEl.textContent = '模拟查询过程中发生错误。';
                 resultsPlaceholderEl.style.display = 'block';
                 resultsContentEl.style.display = 'none';
            }
            isQuerying = false;
        }, queryDelay);
    }

    // --- Data Simulation & Processing ---
    function generateSimulatedData(sensorInfo, startTime, endTime) {
        const data = [];
        const durationMillis = endTime.getTime() - startTime.getTime();
        const pointsToGenerate = Math.min(5000, Math.floor(durationMillis / (1000 * 5))); // Simulate 1 point every 5 seconds, max 5000

        if (pointsToGenerate <= 0) return [];

        const timeStep = durationMillis / pointsToGenerate;

        for (let i = 0; i < pointsToGenerate; i++) {
            const timestamp = new Date(startTime.getTime() + i * timeStep);
            // Simulate value based on a slow sine wave (daily cycle) + faster sine (hourly cycle) + noise
            const timeFractionDay = (timestamp.getHours() * 3600 + timestamp.getMinutes() * 60 + timestamp.getSeconds()) / (24 * 3600);
            const timeFractionHour = (timestamp.getMinutes() * 60 + timestamp.getSeconds()) / 3600;

            const dailyCycle = Math.sin(timeFractionDay * 2 * Math.PI) * (sensorInfo.amplitude * 0.6);
            const hourlyCycle = Math.sin(timeFractionHour * 2 * Math.PI) * (sensorInfo.amplitude * 0.3);
            const noise = (Math.random() - 0.5) * sensorInfo.noise;

            const value = sensorInfo.base + dailyCycle + hourlyCycle + noise;

            data.push({ timestamp, value });
        }
        return data;
    }

    function processAndRenderResults(data) {
        if (!data || data.length === 0) {
            // Should be handled before calling this, but double-check
            resultsContentEl.style.display = 'none';
            resultsPlaceholderEl.textContent = '无数据显示。';
             resultsPlaceholderEl.style.display = 'block';
            return;
        }

        let minTemp = data[0].value;
        let maxTemp = data[0].value;
        let sumTemp = 0;

        data.forEach(point => {
            if (point.value < minTemp) minTemp = point.value;
            if (point.value > maxTemp) maxTemp = point.value;
            sumTemp += point.value;
        });

        const avgTemp = sumTemp / data.length;

        // Update stats
        statMinTempEl.textContent = minTemp.toFixed(1);
        statMaxTempEl.textContent = maxTemp.toFixed(1);
        statAvgTempEl.textContent = avgTemp.toFixed(1);
        statPointCountEl.textContent = data.length.toLocaleString();

        // Update sample data table (show first/last few points)
        sampleDataTableBody.innerHTML = ''; // Clear previous samples
        const sampleSize = 10;
        const samples = data.slice(0, sampleSize / 2).concat(data.slice(-sampleSize / 2));

        samples.forEach(point => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${point.timestamp.toLocaleString('zh-CN')}</td>
                <td>${point.value.toFixed(2)}</td>
            `;
            sampleDataTableBody.appendChild(row);
        });

        // Show results section
        resultsContentEl.style.display = 'flex'; // Use flex for column layout
        resultsPlaceholderEl.style.display = 'none';

         // In a real scenario, update the chart here
         // e.g., updateChart(data);
    }

    // --- UI Updates ---
    function setQueryStatus(statusType, statusText) {
        queryStatusEl.textContent = statusText;
        queryStatusEl.className = `status-${statusType}`;
        querySpinnerEl.style.display = (statusType === 'querying') ? 'inline-block' : 'none';
    }

    // --- Utility Functions ---
    function updateTime() {
        simulationTime = new Date();
        currentTimeEl.textContent = simulationTime.toLocaleTimeString('zh-CN');
    }

    // --- Initial Call ---
    initializeTSDBMonitor();
}); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

地上一の鹅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值