angle_dtu/templates/index.html
2025-09-09 16:02:22 +08:00

707 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测斜仪数据</title>
<script src="/static/js/chart.js"></script>
<link rel="stylesheet" href="/static/css/tailwind.min.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.content-narrow {
width: 85%;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 768px) {
.content-narrow { width: 92%; }
}
.header {
padding: 10px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 15px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.control-row {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 5px;
}
select, input, button {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
/* 覆盖设备列表按钮的全局 button 样式,确保文字不是白色且背景为白色 */
#deviceListInline button {
background-color: #ffffff;
color: #1f2937; /* tailwind: text-gray-800 */
}
#deviceListInline button:hover {
background-color: #f9fafb; /* tailwind: hover:bg-gray-50 */
}
.chart-container {
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
background-color: #fff;
}
canvas {
width: 100%;
max-height: 400px;
}
.table-container {
overflow-x: auto;
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
h2{
text-align:center;
color: #333;
margin-top: 0;
margin-bottom: 15px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: left;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f5f5f5;
}
.footer {
text-align: center;
padding: 10px;
border-top: 1px solid #ddd;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.control-group {
width: 100%;
}
select, input {
width: 100%;
}
}
</style>
</head>
<body class="text-[14px] md:text-[15px]">
<div class="header p-2 text-center border-b border-gray-200">
<h1 class="text-2xl md:text-3xl font-semibold py-7">测斜仪数据</h1>
</div>
<div class="content-narrow py-5">
<!-- 设备列表(内联显示) -->
<div id="devicesInline" class="mb-5 p-3 border rounded bg-white">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-4">
<strong class="text-sm text-gray-700">设备列表</strong>
</div>
<span class="text-xs text-gray-600">最后一次在线时间</span>
</div>
<div id="deviceListInline" class="flex flex-col gap-2"></div>
</div>
<div class="controls flex flex-col gap-4 mb-5 p-4 border rounded bg-white">
<div class="control-row">
<div class="control-group">
<label for="deviceInput" class="text-sm text-gray-600">设备编号:</label>
<input id="deviceInput" type="text" placeholder="" class="px-2 py-1 border border-gray-300 rounded text-sm font-mono" />
<button id="findDeviceBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">搜索</button>
</div>
<div class="control-group">
<label for="sensorSelect" class="text-sm text-gray-600">选择探头:</label>
<select id="sensorSelect" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="all">所有探头</option>
</select>
</div>
<div class="control-group">
<label for="metricSelect" class="text-sm text-gray-600">图例:</label>
<select id="metricSelect" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="x" selected>X 轴</option>
<option value="y">Y 轴</option>
<option value="temperature">温度</option>
</select>
</div>
</div>
<div class="control-row">
<div class="control-group">
<label for="limitSelect" class="text-sm text-gray-600">显示记录数:</label>
<select id="limitSelect" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="100">100</option>
<option value="200">200</option>
<option value="400">400</option>
<option value="800">800</option>
<option value="1600">1600</option>
<option value="all" selected>全部</option>
</select>
</div>
<div class="control-group">
<label for="startDate" class="text-sm text-gray-600">开始时间:</label>
<input type="datetime-local" id="startDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
</div>
<div class="control-group">
<label for="endDate" class="text-sm text-gray-600">结束时间:</label>
<input type="datetime-local" id="endDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
</div>
<div class="control-group">
<button id="queryBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm">查询数据</button>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="chart-container rounded-md">
<!-- <h2>传感器数据图表</h2>-->
<canvas id="sensorChart"></canvas>
</div>
<!-- 数据表格 -->
<div class="table-container">
<table id="dataTable" class="min-w-full text-sm text-center">
<thead>
<tr>
<th>数据编号</th>
<th>探头地址</th>
<th>时间</th>
<th>X</th>
<th>Y</th>
<th>温度(°C)</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- 数据将通过JavaScript动态添加 -->
</tbody>
</table>
</div>
</div>
<!-- 内联JavaScript -->
<script>
let sensorChart = null;
let allSensors = [];
let currentSensorData = [];
let devices = [];
let selectedDeviceID = '';
let selectedMetric = 'x';
document.addEventListener('DOMContentLoaded', function() {
initializeDatePickers();
setupEventListeners();
loadDevices();
});
function initializeDatePickers() {
const now = new Date();
const twoDaysMs = 2 * 24 * 60 * 60 * 1000;
const begin = new Date(now.getTime() - twoDaysMs);
function fmt(dt) {
const y = dt.getFullYear();
const m = (dt.getMonth() + 1).toString().padStart(2, '0');
const d = dt.getDate().toString().padStart(2, '0');
const hh = dt.getHours().toString().padStart(2, '0');
const mm = dt.getMinutes().toString().padStart(2, '0');
return `${y}-${m}-${d}T${hh}:${mm}`;
}
document.getElementById('startDate').value = fmt(begin);
document.getElementById('endDate').value = fmt(now);
}
function setupEventListeners() {
document.getElementById('queryBtn').addEventListener('click', function() {
loadData();
});
document.getElementById('sensorSelect').addEventListener('change', function() {
loadData();
});
const deviceInput = document.getElementById('deviceInput');
const findBtn = document.getElementById('findDeviceBtn');
if (findBtn) findBtn.addEventListener('click', findDeviceByInput);
if (deviceInput) deviceInput.addEventListener('keydown', function(e){ if (e.key === 'Enter') { findDeviceByInput(); }});
const metricSelect = document.getElementById('metricSelect');
if (metricSelect) metricSelect.addEventListener('change', function(){ selectedMetric = this.value; updateChart(currentSensorData); });
// 统计信息已融合到设备列表区域,无需额外滚动链接
}
function findDeviceByInput() {
const input = document.getElementById('deviceInput');
if (!input) return;
const v = (input.value || '').trim();
if (!v) { alert('请输入设备编号'); return; }
if (!devices || devices.length === 0) {
loadDevices();
}
const match = devices.find(d => (d.device_id || '').toLowerCase() === v.toLowerCase());
if (!match) { alert('未找到该设备'); return; }
selectedDeviceID = match.device_id;
loadSensors();
}
function loadSensors() {
if (!selectedDeviceID) return;
fetch(`/api/sensors?device_id=${encodeURIComponent(selectedDeviceID)}`)
.then(response => {
if (!response.ok) {
throw new Error('获取传感器列表失败');
}
return response.json();
})
.then(data => {
allSensors = data;
updateSensorSelect(data);
loadData();
})
.catch(error => {
console.error('加载传感器列表出错:', error);
alert('加载传感器列表出错: ' + error.message);
});
}
function updateSensorSelect(sensors) {
const select = document.getElementById('sensorSelect');
const allOption = select.querySelector('option[value="all"]');
select.innerHTML = '';
select.appendChild(allOption);
if (sensors.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = '没有可用的传感器';
select.appendChild(option);
return;
}
sensors.forEach(id => {
const option = document.createElement('option');
option.value = id;
option.textContent = `探头 ${id}`;
select.appendChild(option);
});
}
function loadData() {
if (!selectedDeviceID) { alert('请先选择设备'); return; }
const sensorID = document.getElementById('sensorSelect').value;
const limit = document.getElementById('limitSelect').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
let url = '/api/data?';
let params = [];
params.push(`device_id=${encodeURIComponent(selectedDeviceID)}`);
if (sensorID !== 'all') {
params.push(`sensor_id=${sensorID}`);
}
params.push(`limit=${limit}`);
if (startDate) {
params.push(`start_date=${encodeURIComponent(startDate)}`);
}
if (endDate) {
params.push(`end_date=${encodeURIComponent(endDate)}`);
}
url += params.join('&');
document.getElementById('queryBtn').textContent = '加载中...';
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('获取传感器数据失败');
}
return response.json();
})
.then(data => {
currentSensorData = data;
updateTable(data);
updateChart(data);
document.getElementById('queryBtn').textContent = '查询数据';
})
.catch(error => {
console.error('加载数据出错:', error);
alert('加载数据出错: ' + error.message);
document.getElementById('queryBtn').textContent = '查询数据';
});
}
// 渲染内联设备列表(按最后上报排序,新→旧)
function renderDeviceListInline() {
const container = document.getElementById('deviceListInline');
if (!container) return;
container.innerHTML = '';
if (!devices || devices.length === 0) {
const empty = document.createElement('div');
empty.className = 'text-gray-500 text-sm';
empty.textContent = '没有设备';
container.appendChild(empty);
return;
}
const view = devices.slice().sort((a,b) => {
const ta = a.last_seen ? new Date(a.last_seen).getTime() : 0;
const tb = b.last_seen ? new Date(b.last_seen).getTime() : 0;
return tb - ta; // 新→旧
});
view.forEach(d => {
const onlineDot = d.online ? '<span class=\"inline-block w-2 h-2 rounded-full bg-green-500\"></span>' : '<span class=\"inline-block w-2 h-2 rounded-full bg-gray-400\"></span>';
const lastSeen = (() => {
if (!d.last_seen) return '—';
const dt = new Date(d.last_seen);
const y = dt.getFullYear();
const m = (dt.getMonth()+1).toString().padStart(2,'0');
const day = dt.getDate().toString().padStart(2,'0');
const hh = dt.getHours().toString().padStart(2,'0');
const mm = dt.getMinutes().toString().padStart(2,'0');
const ss = dt.getSeconds().toString().padStart(2,'0');
return `${y}/${m}/${day} ${hh}:${mm}:${ss}`;
})();
const isSelected = d.device_id === selectedDeviceID;
const item = document.createElement('button');
item.type = 'button';
// 一行一个,左右布局:左侧设备信息,右侧最后在线时间
item.className = 'w-full px-3 py-2 border rounded text-sm bg-white hover:bg-gray-50 flex items-center justify-between transition text-gray-800 ' + (isSelected ? 'ring-2 ring-blue-400' : '');
const leftHtml = `<span class=\"flex items-center gap-2\">${onlineDot}
<span class=\"font-mono\">${d.device_id}</span>
<span class=\"text-xs text-gray-600\">(探头: ${d.sensor_count})</span></span>`;
const rightHtml = `<span class=\"text-xs text-gray-700 font-mono\">${lastSeen}</span>`;
item.innerHTML = leftHtml + rightHtml;
item.addEventListener('click', () => {
selectedDeviceID = d.device_id;
const input = document.getElementById('deviceInput');
if (input) input.value = selectedDeviceID;
renderDeviceListInline();
loadSensors();
});
container.appendChild(item);
});
}
// updateDeviceList 已移除,改用 renderDeviceListInline()
function loadDevices() {
fetch('/api/devices')
.then(r => r.json())
.then(data => {
devices = data || [];
const total = document.getElementById('totalDevices');
if (total) total.textContent = devices.length;
const onlineEl = document.getElementById('onlineDevices');
if (onlineEl) onlineEl.textContent = devices.filter(d => d.online).length;
renderDeviceListInline();
// 默认选择 1513343 设备(若存在)并自动加载
if (!selectedDeviceID) {
const def = devices.find(d => (d.device_id || '').toLowerCase() === '1513343');
if (def) {
selectedDeviceID = def.device_id;
const input = document.getElementById('deviceInput');
if (input) input.value = selectedDeviceID;
loadSensors();
}
}
})
.catch(err => console.error('加载设备失败:', err));
}
function updateTable(data) {
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
if (data.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="6" style="text-align: center;">没有数据</td>';
tableBody.appendChild(row);
return;
}
data.forEach(item => {
const row = document.createElement('tr');
const date = new Date(item.timestamp);
const formattedDate =
date.getFullYear() + '/' +
(date.getMonth() + 1).toString().padStart(2, '0') + '/' +
date.getDate().toString().padStart(2, '0') + ' ' +
date.getHours().toString().padStart(2, '0') + ':' +
date.getMinutes().toString().padStart(2, '0') + ':' +
date.getSeconds().toString().padStart(2, '0');
row.innerHTML =
'<td>' + item.id + '</td>' +
'<td>' + item.sensor_id + '</td>' +
'<td>' + formattedDate + '</td>' +
'<td>' + item.x.toFixed(3) + '</td>' +
'<td>' + item.y.toFixed(3) + '</td>' +
'<td>' + item.temperature.toFixed(1) + '</td>';
tableBody.appendChild(row);
});
}
function updateChart(data) {
const chartData = prepareChartData(data);
const sensorSelect = document.getElementById('sensorSelect');
const singleSensorSelected = sensorSelect && sensorSelect.value !== 'all';
const stats = singleSensorSelected ? computeCurrentStddev(data) : { stddev: 0, count: 0, min: null, max: null };
const fmt = (v) => (selectedMetric === 'temperature' ? v.toFixed(1) : v.toFixed(3));
const stdText = (singleSensorSelected && stats.count > 1)
? `最大: ${fmt(stats.max)} 最小: ${fmt(stats.min)} 标准差: ${fmt(stats.stddev)} (n=${stats.count})`
: '';
if (sensorChart) {
sensorChart.destroy();
}
const ctx = document.getElementById('sensorChart').getContext('2d');
sensorChart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: (selectedMetric === 'temperature' ? '温度' : selectedMetric.toUpperCase() + ' 轴')
},
subtitle: {
display: singleSensorSelected && stdText !== '',
text: stdText,
align: 'end',
color: '#666',
padding: {bottom: 8},
font: {size: 12}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
if (selectedMetric === 'temperature') {
label += context.parsed.y.toFixed(1);
} else {
label += context.parsed.y.toFixed(3);
}
}
return label;
}
}
}
},
scales: {
x: {
title: {
display: true,
text: '时间'
}
},
y: {
title: {
display: true,
text: selectedMetric === 'temperature' ? '温度(°C)' : selectedMetric.toUpperCase()
}
}
}
}
});
}
function computeCurrentStddev(data) {
if (!Array.isArray(data) || data.length === 0) return {stddev: 0, count: 0, min: null, max: null};
const sidVal = (document.getElementById('sensorSelect') || {}).value || 'all';
const filterSid = sidVal !== 'all' ? parseInt(sidVal) : null;
let n = 0, mean = 0, M2 = 0, min = Infinity, max = -Infinity;
for (const row of data) {
if (filterSid !== null && row.sensor_id !== filterSid) continue;
const v = row[selectedMetric];
if (typeof v !== 'number' || isNaN(v)) continue;
n += 1;
if (v < min) min = v;
if (v > max) max = v;
const delta = v - mean;
mean += delta / n;
const delta2 = v - mean;
M2 += delta * delta2;
}
if (n < 2) return {stddev: 0, count: n, min: n ? min : null, max: n ? max : null};
const variance = M2 / (n - 1);
return {stddev: Math.sqrt(variance), count: n, min, max};
}
function prepareChartData(data) {
if (!Array.isArray(data) || data.length === 0) {
return { labels: [], datasets: [] };
}
const sortedData = [...data].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
const sensorIDs = [...new Set(sortedData.map(item => item.sensor_id))];
let labels = [];
if (sensorIDs.length > 0) {
const firstSensorData = sortedData.filter(item => item.sensor_id === sensorIDs[0]);
labels = firstSensorData.map(item => {
const date = new Date(item.timestamp);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
});
}
const colors = [
'rgb(75, 192, 192)',
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)',
'rgb(153, 102, 255)',
'rgb(255, 159, 64)'
];
const datasets = sensorIDs.map((sid, idx) => {
const sensorData = sortedData.filter(item => item.sensor_id === sid);
const color = colors[idx % colors.length];
return {
label: `探头 ${sid}`,
data: sensorData.map(item => item[selectedMetric]),
fill: false,
borderColor: color,
tension: 0.1
};
});
return { labels, datasets };
}
</script>
</body>
</html>