angle_dtu/templates/index.html
2025-05-24 13:41:43 +08:00

890 lines
34 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="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.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-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;
padding: 15px;
}
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;
}
/* 加载动画样式 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.countdown {
font-size: 14px;
color: #666;
margin-left: 10px;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.control-group {
width: 100%;
}
select, input {
width: 100%;
}
}
</style>
</head>
<body>
<div class="header">
<h1>测斜仪数据</h1>
<div id="connectionStatus" style="display: inline-block; padding: 5px 10px; border-radius: 4px; margin-left: 10px; background-color: red; color: white;">
未连接
</div>
</div>
<div class="container">
<div class="controls">
<div class="control-group">
<label for="sensorSelect">选择探头:</label>
<select id="sensorSelect">
<option value="all">所有探头</option>
</select>
</div>
<div class="control-group">
<label for="limitSelect">显示记录数:</label>
<select id="limitSelect">
<option value="10">10</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500" selected>500</option>
<option value="1000">1000</option>
<option value="2000">2000</option>
<option value="5000">5000</option>
<option value="50000">50000</option>
<option value="all">全部</option>
</select>
</div>
<div class="control-group">
<label for="startDate">开始日期:</label>
<input type="datetime-local" id="startDate">
</div>
<div class="control-group">
<label for="endDate">结束日期:</label>
<input type="datetime-local" id="endDate">
</div>
<div class="control-group">
<button id="queryBtn">查询数据</button>
<button id="resetBtn">重置筛选</button>
<button id="queryLatestBtn">查询最新数据</button>
<button id="exportBtn">导出CSV</button>
</div>
<div class="control-group">
<label>
<input type="checkbox" id="autoRefresh">
自动刷新 (10分钟周期)
</label>
</div>
</div>
<!-- 最新数据显示区域 -->
<div class="latest-data" style="margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background-color: #f8f9fa;">
<h3 style="margin-top: 0; margin-bottom: 10px; color: #333;">最新传感器数据 <span id="latest-time" style="font-weight: normal; font-size: 14px;"></span></h3>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;">
<div style="padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: white;">
<div style="font-weight: bold; color: #555; font-size: 12px;">传感器ID</div>
<div style="font-size: 20px; color: #555;" id="latest-sensor-id">--</div>
</div>
<div style="padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: white;">
<div style="font-weight: bold; color: #555; font-size: 12px;">X轴值</div>
<div style="font-size: 20px; color: #555;" id="latest-x">--</div>
</div>
<div style="padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: white;">
<div style="font-weight: bold; color: #555; font-size: 12px;">Y轴值</div>
<div style="font-size: 20px; color: #555;" id="latest-y">--</div>
</div>
<div style="padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: white;">
<div style="font-weight: bold; color: #555; font-size: 12px;">Z轴值</div>
<div style="font-size: 20px; color: #555;" id="latest-z">--</div>
</div>
<div style="padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: white;">
<div style="font-weight: bold; color: #555; font-size: 12px;">温度</div>
<div style="font-size: 20px; color: #555;" id="latest-temperature">--</div>
<div style="font-size: 12px; color: #777;"></div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="chart-container">
<h2>传感器数据图表</h2>
<canvas id="sensorChart"></canvas>
</div>
<!-- 数据表格 -->
<div class="table-container">
<h2>传感器数据表格</h2>
<table id="dataTable">
<thead>
<tr>
<th>数据编号</th>
<th>探头地址</th>
<th>时间</th>
<th>X</th>
<th>Y</th>
<th>Z</th>
<th>温度(°C)</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- 数据将通过JavaScript动态添加 -->
</tbody>
</table>
</div>
</div>
<!-- 内联JavaScript -->
<script>
// 全局变量
let sensorChart = null;
let refreshInterval = null;
let allSensors = [];
let currentSensorData = [];
let connectionCheckTimer = null;
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化日期选择器为今天
initializeDatePickers();
// 加载所有传感器ID
loadSensors();
// 添加事件监听器
setupEventListeners();
// 设置自动刷新
setupAutoRefresh();
// 检查连接状态
checkConnectionStatus();
connectionCheckTimer = setInterval(checkConnectionStatus, 30000); // 每30秒检查一次
// 显示数据库中的最新数据不发送TCP查询指令
fetchLatestData();
});
// 初始化日期选择器
function initializeDatePickers() {
// 获取当前时间(使用本地时间)
const now = new Date();
// 格式化日期和时间为HTML datetime-local输入格式 YYYY-MM-DDThh:mm
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0'); // 月份从0开始
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const today = `${year}-${month}-${day}`;
const currentTime = `${hours}:${minutes}`;
// 设置默认的开始时间为当天00:00
document.getElementById('startDate').value = `${today}T00:00`;
// 设置默认的结束时间为当前时间
document.getElementById('endDate').value = `${today}T${currentTime}`;
console.log("当前设置的开始时间:", document.getElementById('startDate').value);
console.log("当前设置的结束时间:", document.getElementById('endDate').value);
}
// 设置事件监听器
function setupEventListeners() {
// 查询按钮
document.getElementById('queryBtn').addEventListener('click', function() {
loadData();
});
// 重置按钮
document.getElementById('resetBtn').addEventListener('click', function() {
resetFilters();
});
// 查询最新数据按钮
document.getElementById('queryLatestBtn').addEventListener('click', function() {
queryLatestData();
});
// 传感器选择变化
document.getElementById('sensorSelect').addEventListener('change', function() {
loadData();
});
// 记录数限制变化
document.getElementById('limitSelect').addEventListener('change', function() {
loadData();
});
// 导出CSV按钮
document.getElementById('exportBtn').addEventListener('click', function() {
exportToCSV();
});
}
// 设置自动刷新
function setupAutoRefresh() {
const autoRefreshCheckbox = document.getElementById('autoRefresh');
// 初始化自动刷新
if (autoRefreshCheckbox.checked) {
startAutoRefresh();
}
// 监听复选框变化
autoRefreshCheckbox.addEventListener('change', function() {
if (this.checked) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
}
// 开始自动刷新
function startAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
refreshInterval = setInterval(loadData, 1000 * 60 * 10);
}
// 停止自动刷新
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// 重置筛选条件
function resetFilters() {
initializeDatePickers();
document.getElementById('sensorSelect').value = 'all';
document.getElementById('limitSelect').value = '500';
loadData();
}
// 加载所有传感器ID
function loadSensors() {
fetch('/api/sensors')
.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() {
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 = [];
// 添加查询参数
if (sensorID !== 'all') {
params.push(`sensor_id=${sensorID}`);
}
// 始终发送limit参数即使是'all'
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 updateTable(data) {
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
if (data.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="7" style="text-align: center;">没有数据</td>';
tableBody.appendChild(row);
return;
}
data.forEach(item => {
const row = document.createElement('tr');
// 解析时间并调整为中国时间UTC+8
const date = new Date(item.timestamp);
// 减去8小时因为数据库时间似乎比实际时间早了8小时
date.setHours(date.getHours() - 8);
// 格式化为中文日期时间格式
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.z.toFixed(3) + '</td>' +
'<td>' + item.temperature.toFixed(1) + '</td>';
tableBody.appendChild(row);
});
}
// 更新图表
function updateChart(data) {
// 准备图表数据
const chartData = prepareChartData(data);
// 如果图表已经存在,销毁它
if (sensorChart) {
sensorChart.destroy();
}
// 获取图表Canvas
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: '历史数据'
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(3);
}
return label;
}
}
}
},
scales: {
x: {
title: {
display: true,
text: '时间'
}
},
y: {
title: {
display: true,
text: '值'
}
}
}
}
});
}
// 准备图表数据
function prepareChartData(data) {
// 如果没有数据,返回空数据集
if (data.length === 0) {
return {
labels: [],
datasets: []
};
}
// 反转数据以便按时间先后顺序显示
const sortedData = [...data].sort((a, b) => {
return new Date(a.timestamp) - new Date(b.timestamp);
});
// 获取所有传感器ID
let sensorIDs = [...new Set(sortedData.map(item => item.sensor_id))];
// 按传感器ID分组数据
let datasets = [];
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);
date.setHours(date.getHours() - 8); // 调整时区
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)'
];
// 为X, Y, Z, 温度创建不同的数据集
const dataTypes = [
{ key: 'x', label: 'X' },
{ key: 'y', label: 'Y' },
{ key: 'z', label: 'Z' },
{ key: 'temperature', label: '温度(°C)' }
];
sensorIDs.forEach((sensorID, sensorIndex) => {
const sensorData = sortedData.filter(item => item.sensor_id === sensorID);
dataTypes.forEach((type, typeIndex) => {
const colorIndex = (sensorIndex * dataTypes.length + typeIndex) % colors.length;
datasets.push({
label: `探头${sensorID} - ${type.label}`,
data: sensorData.map(item => item[type.key]),
fill: false,
borderColor: colors[colorIndex],
tension: 0.1
});
});
});
return {
labels: labels,
datasets: datasets
};
}
// 导出到CSV文件
function exportToCSV() {
if (currentSensorData.length === 0) {
alert('没有数据可导出');
return;
}
// 准备CSV内容
let csvContent = "数据编号,探头地址编号,X,Y,Z,温度,时间\n";
currentSensorData.forEach(item => {
// 解析时间并调整为中国时间
const date = new Date(item.timestamp);
date.setHours(date.getHours() - 8);
// 格式化日期
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');
csvContent += item.id + "," +
item.sensor_id + "," +
item.x.toFixed(3) + "," +
item.y.toFixed(3) + "," +
item.z.toFixed(3) + "," +
item.temperature.toFixed(1) + "," +
formattedDate + "\n";
});
// 创建Blob对象
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// 创建下载链接
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
// 设置下载属性
link.setAttribute("href", url);
link.setAttribute("download", "sensor_data.csv");
// 添加到文档并点击
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
}
// 检查连接状态
function checkConnectionStatus() {
fetch('/api/clients')
.then(response => response.json())
.then(data => {
const statusElem = document.getElementById('connectionStatus');
if (data && data.length > 0) {
const onlineClients = data.filter(client => client.isOnline);
if (onlineClients.length > 0) {
statusElem.style.backgroundColor = 'green';
const client = onlineClients[0];
if (onlineClients.length > 1) {
statusElem.textContent = `已连接: ${client.ip}:${client.port} (共${onlineClients.length}个设备)`;
} else {
statusElem.textContent = `已连接: ${client.ip}:${client.port}`;
}
} else {
statusElem.style.backgroundColor = 'orange';
statusElem.textContent = '设备离线';
}
} else {
statusElem.style.backgroundColor = 'red';
statusElem.textContent = '未连接';
}
})
.catch(error => {
console.error('获取连接状态失败:', error);
const statusElem = document.getElementById('connectionStatus');
statusElem.style.backgroundColor = 'red';
statusElem.textContent = '状态未知';
});
}
// 获取最新传感器数据
function fetchLatestData() {
fetch('/api/data?limit=1')
.then(response => response.json())
.then(data => {
if (data && data.length > 0) {
const latest = data[0];
updateLatestDataDisplay(latest);
}
})
.catch(error => {
console.error('获取最新数据失败:', error);
});
}
// 更新最新数据显示
function updateLatestDataDisplay(data) {
const date = new Date(data.timestamp);
date.setHours(date.getHours() - 8);
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');
document.getElementById('latest-time').textContent = `(${formattedDate})`;
document.getElementById('latest-sensor-id').textContent = data.sensor_id;
document.getElementById('latest-x').textContent = data.x.toFixed(3);
document.getElementById('latest-y').textContent = data.y.toFixed(3);
document.getElementById('latest-z').textContent = data.z.toFixed(3);
document.getElementById('latest-temperature').textContent = data.temperature.toFixed(1);
}
// 查询最新数据(触发设备查询)
function queryLatestData() {
const latestDataElement = document.querySelector('.latest-data');
const queryBtn = document.getElementById('queryLatestBtn');
latestDataElement.style.opacity = 0.6;
// 添加加载动画和倒计时
const originalText = queryBtn.innerHTML;
queryBtn.innerHTML = '查询中<span class="loading-spinner"></span>';
queryBtn.disabled = true;
// 先获取一次当前最新数据
fetchLatestData();
// 发送TCP查询指令
fetch('/api/trigger-query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`查询指令已发送到 ${data.sent_count} 个设备`);
if (data.sent_count === 0) {
// 没有设备在线
latestDataElement.style.opacity = 1;
queryBtn.innerHTML = originalText;
queryBtn.disabled = false;
alert('当前没有设备在线,无法发送查询指令');
return;
}
// 开始10秒倒计时
let countdown = 10;
queryBtn.innerHTML = `等待设备响应<span class="loading-spinner"></span><span class="countdown">${countdown}秒</span>`;
const countdownTimer = setInterval(() => {
countdown--;
if (countdown > 0) {
queryBtn.innerHTML = `等待设备响应<span class="loading-spinner"></span><span class="countdown">${countdown}秒</span>`;
} else {
clearInterval(countdownTimer);
queryBtn.innerHTML = '获取最新数据中<span class="loading-spinner"></span>';
}
}, 1000);
// 等待10秒后获取最新数据给设备响应时间
setTimeout(() => {
fetchLatestData();
loadData(); // 也刷新一下表格数据
latestDataElement.style.opacity = 1;
queryBtn.innerHTML = originalText;
queryBtn.disabled = false;
clearInterval(countdownTimer);
}, 10000);
} else {
console.error('发送查询指令失败:', data.message);
latestDataElement.style.opacity = 1;
queryBtn.innerHTML = originalText;
queryBtn.disabled = false;
}
})
.catch(error => {
console.error('发送查询指令出错:', error);
latestDataElement.style.opacity = 1;
queryBtn.innerHTML = originalText;
queryBtn.disabled = false;
});
}
// 页面卸载时清除定时器
window.addEventListener('beforeunload', function() {
if (connectionCheckTimer) {
clearInterval(connectionCheckTimer);
}
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
</body>
</html>