angle_dtu/templates/index.html
2025-09-08 19:16:07 +08:00

724 lines
28 KiB
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>
<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;
}
.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 class="system-info bg-gray-100 p-3 mb-5 rounded text-sm leading-6">
<strong>在线设备:</strong> <span id="onlineDevices">0</span>
<span class="mx-2">|</span>
<strong>总设备:</strong> <a href="#" id="showDeviceList" class="text-blue-600 hover:text-blue-700"><span id="totalDevices">0</span></a>
</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 id="deviceModal" class="hidden fixed inset-0 bg-black/30 z-[2000] items-center justify-center">
<div class="bg-white w-[90%] max-w-[720px] max-h-[70vh] rounded-lg shadow-2xl flex flex-col">
<div class="px-5 py-3 border-b border-gray-200 relative">
<strong class="text-sm">选择设备</strong>
<button id="closeDeviceModal" class="absolute right-5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"></button>
</div>
<div id="deviceList" class="flex-1 overflow-y-auto py-2"></div>
<div class="px-3 py-2 border-t border-gray-200 flex items-center justify-between bg-white">
<div class="flex items-center gap-3 text-xs text-gray-600">
<button id="prevPage" class="px-2 py-1 border border-gray-300 rounded bg-white hover:bg-gray-50">&lt; 上一页</button>
<span><span id="currentPage">1</span> 页,共 <span id="totalPages">1</span></span>
<button id="nextPage" class="px-2 py-1 border border-gray-300 rounded bg-white hover:bg-gray-50">下一页 &gt;</button>
</div>
</div>
</div>
</div>
</div>
<!-- 内联JavaScript -->
<script>
let sensorChart = null;
let allSensors = [];
let currentSensorData = [];
let devices = [];
let selectedDeviceID = '';
let currentPage = 1;
let itemsPerPage = 10;
let selectedMetric = 'x';
document.addEventListener('DOMContentLoaded', function() {
initializeDatePickers();
setupEventListeners();
loadDevices();
});
function initializeDatePickers() {
const now = new Date();
const tenDaysMs = 10 * 24 * 60 * 60 * 1000;
const begin = new Date(now.getTime() - tenDaysMs);
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); });
const openBtn = document.getElementById('openDeviceModal');
const showList = document.getElementById('showDeviceList');
const closeBtn = document.getElementById('closeDeviceModal');
if (openBtn) openBtn.addEventListener('click', openDeviceModal);
if (showList) showList.addEventListener('click', function(e){ e.preventDefault(); openDeviceModal(); });
if (closeBtn) closeBtn.addEventListener('click', closeDeviceModal);
const prev = document.getElementById('prevPage');
const next = document.getElementById('nextPage');
if (prev) prev.addEventListener('click', function(){ updateDeviceList(currentPage - 1); });
if (next) next.addEventListener('click', function(){ updateDeviceList(currentPage + 1); });
const modal = document.getElementById('deviceModal');
if (modal) modal.addEventListener('click', function(e){ if (e.target === modal) { closeDeviceModal(); }});
}
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 openDeviceModal() {
const modal = document.getElementById('deviceModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
updateDeviceList(1);
}
}
function closeDeviceModal() {
const modal = document.getElementById('deviceModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function updateDeviceList(page) {
if (!devices || devices.length === 0) return;
const totalPages = Math.max(1, Math.ceil(devices.length / itemsPerPage));
currentPage = Math.min(Math.max(1, page), totalPages);
const cur = document.getElementById('currentPage');
const tot = document.getElementById('totalPages');
if (cur) cur.textContent = currentPage;
if (tot) tot.textContent = totalPages;
const prev = document.getElementById('prevPage');
const next = document.getElementById('nextPage');
if (prev) prev.disabled = currentPage <= 1;
if (next) next.disabled = currentPage >= totalPages;
const startIdx = (currentPage - 1) * itemsPerPage;
const endIdx = Math.min(startIdx + itemsPerPage, devices.length);
const list = devices.slice(startIdx, endIdx);
const container = document.getElementById('deviceList');
if (!container) return;
container.innerHTML = '';
list.forEach(d => {
const onlineDot = d.online ? '<span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>' : '<span class="inline-block w-2 h-2 rounded-full bg-gray-400 mr-2"></span>';
const lastSeen = d.last_seen ? new Date(d.last_seen).toLocaleString() : '—';
const item = document.createElement('div');
item.className = 'device-item px-5 py-3 border-b border-gray-100 flex justify-between items-center cursor-pointer hover:bg-gray-50';
item.innerHTML = `
<div class="flex items-center gap-2">
${onlineDot}
<span class="font-mono">${d.device_id}</span>
<span class="text-xs text-gray-500">(探头: ${d.sensor_count})</span>
</div>
<div class="text-xs text-gray-500">最后上报: ${lastSeen}</div>
`;
item.addEventListener('click', () => {
selectedDeviceID = d.device_id;
const input = document.getElementById('deviceInput');
if (input) input.value = selectedDeviceID;
closeDeviceModal();
loadSensors();
});
container.appendChild(item);
});
}
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;
// 默认选择 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>