rain_monitor/static/index.html
2025-07-10 12:19:35 +08:00

763 lines
31 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;
}
.latest-data {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f8f9fa;
}
.latest-data h3 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.data-item {
padding: 8px;
border: 1px solid #eee;
border-radius: 4px;
background-color: white;
}
.data-label {
font-weight: bold;
color: #555;
font-size: 12px;
}
.data-value {
font-size: 20px;
color: #555;
}
.data-unit {
font-size: 12px;
color: #777;
}
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;
}
.table-container {
overflow-x: auto;
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
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;
}
@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="interval">数据粒度:</label>
<select id="interval">
<option value="raw">原始数据</option>
<option value="1min">1分钟测试用</option>
<option value="5min">5分钟</option>
<option value="30min">30分钟</option>
<option value="1hour" selected>1小时</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 onclick="queryData()">查询历史数据</button>
<button onclick="queryLatestData()">查询最新数据</button>
<button onclick="exportData()">导出数据</button>
</div>
</div>
<!-- 最新数据显示区域 -->
<div class="latest-data">
<h3>最新传感器数据 <span id="latest-time" style="font-weight: normal; font-size: 14px;"></span></h3>
<div class="data-grid">
<div class="data-item">
<div class="data-label">温度</div>
<div class="data-value" id="latest-temperature">--</div>
<div class="data-unit"></div>
</div>
<div class="data-item">
<div class="data-label">湿度</div>
<div class="data-value" id="latest-humidity">--</div>
<div class="data-unit">%</div>
</div>
<div class="data-item">
<div class="data-label">风速</div>
<div class="data-value" id="latest-wind-speed">--</div>
<div class="data-unit">m/s</div>
</div>
<div class="data-item">
<div class="data-label">风向</div>
<div class="data-value" id="latest-wind-direction">--</div>
<div class="data-unit">°</div>
</div>
<div class="data-item">
<div class="data-label">大气压</div>
<div class="data-value" id="latest-atm-pressure">--</div>
<div class="data-unit">kPa</div>
</div>
<div class="data-item">
<div class="data-label">太阳辐射</div>
<div class="data-value" id="latest-solar-radiation">--</div>
<div class="data-unit">W/m²</div>
</div>
<div class="data-item">
<div class="data-label">累计雨量</div>
<div class="data-value" id="latest-rainfall">--</div>
<div class="data-unit">mm</div>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="mainChart"></canvas>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>时间</th>
<th>小时降雨量(mm)</th>
<th>温度(℃)</th>
<th>湿度(%)</th>
<th>风速(m/s)</th>
<th>大气压(kPa)</th>
<th>太阳辐射(W/m²)</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
<script>
let mainChart = null;
let connectionCheckTimer = null;
// 检查连接状态
function checkConnectionStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const statusElem = document.getElementById('connectionStatus');
if (data.connected) {
statusElem.style.backgroundColor = 'green';
if (data.count > 1) {
statusElem.textContent = `已连接: ${data.ip}:${data.port} (共${data.count}个设备)`;
} else {
statusElem.textContent = `已连接: ${data.ip}:${data.port}`;
}
} else {
statusElem.style.backgroundColor = 'red';
statusElem.textContent = '未连接';
}
})
.catch(error => {
console.error('获取连接状态失败:', error);
const statusElem = document.getElementById('connectionStatus');
statusElem.style.backgroundColor = 'red';
statusElem.textContent = '状态未知';
});
}
// 初始化日期选择器
function initDatePickers() {
// 获取当前北京时间UTC+8
const now = new Date();
// 设置开始时间为当天的0点北京时间
const today = new Date(now);
today.setHours(0, 0, 0, 0);
document.getElementById('startDate').value = formatDateTime(today);
document.getElementById('endDate').value = formatDateTime(now);
}
// 格式化日期时间为中国时区UTC+8
function formatDateTime(date) {
// 转换为ISO字符串但不使用Z表示UTC而是使用+08:00表示中国时区
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// 查询历史数据
function queryData() {
const interval = document.getElementById('interval').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
alert('请选择开始和结束时间');
return;
}
// 确保时间格式符合后端要求,添加本地时区信息
const startDateTime = new Date(startDate).toISOString();
const endDateTime = new Date(endDate).toISOString();
// 加载状态指示
document.getElementById('mainChart').style.opacity = 0.5;
fetch(`/api/data?interval=${interval}&start=${startDateTime}&end=${endDateTime}`)
.then(response => response.json())
.then(data => {
updateChart(data);
updateTable(data);
// 恢复正常显示
document.getElementById('mainChart').style.opacity = 1;
})
.catch(error => {
console.error('Error:', error);
alert('获取数据失败,请检查网络连接');
// 恢复正常显示
document.getElementById('mainChart').style.opacity = 1;
});
}
// 获取最新传感器数据(不触发设备查询)
function fetchLatestSensorData() {
fetch('/api/raw/latest')
.then(response => response.json())
.then(data => {
if (data.error) {
console.log('获取最新传感器数据失败:', data.error);
return;
}
if (!data || !data.timestamp) {
console.log('No latest sensor data available');
return;
}
// 更新最新数据显示
document.getElementById('latest-time').textContent = `(${data.formatted_time})`;
document.getElementById('latest-temperature').textContent = data.temperature ? data.temperature.toFixed(1) : '--';
document.getElementById('latest-humidity').textContent = data.humidity ? data.humidity.toFixed(1) : '--';
document.getElementById('latest-wind-speed').textContent = data.wind_speed ? data.wind_speed.toFixed(2) : '--';
document.getElementById('latest-wind-direction').textContent = data.wind_direction_360 || '--';
document.getElementById('latest-atm-pressure').textContent = data.atm_pressure ? data.atm_pressure.toFixed(1) : '--';
document.getElementById('latest-solar-radiation').textContent = data.solar_radiation || '--';
// 使用 total_rainfall 作为累计雨量,如果没有则回退到 rainfall
document.getElementById('latest-rainfall').textContent = data.total_rainfall !== undefined ? data.total_rainfall.toFixed(1) : (data.rainfall ? data.rainfall.toFixed(1) : '--');
})
.catch(error => {
console.error('获取最新传感器数据失败:', error);
});
}
// 触发设备进行数据查询
function triggerDeviceQuery() {
return fetch('/api/trigger-query')
.then(response => response.json())
.then(data => {
console.log('触发设备查询结果:', data);
if (data.success) {
// 如果成功触发查询等待3秒后获取新数据
return new Promise(resolve => {
setTimeout(() => {
console.log('等待3秒后获取新数据');
resolve(data);
}, 3000);
});
}
return data;
})
.catch(error => {
console.error('触发设备查询失败:', error);
return { success: false, message: '触发设备查询失败' };
});
}
// 触发设备进行数据查询并获取最新数据
function triggerQueryAndFetchData() {
const latestDataElement = document.querySelector('.latest-data');
latestDataElement.style.opacity = 0.5;
// 触发设备查询
return triggerDeviceQuery()
.then(() => {
// 获取最新传感器数据
return fetchLatestSensorData();
})
.then(() => {
// 恢复最新数据区域的不透明度
latestDataElement.style.opacity = 1;
})
.catch(error => {
console.error('触发查询并获取数据失败:', error);
// 恢复最新数据区域的不透明度
latestDataElement.style.opacity = 1;
});
}
// 查询最新数据
function queryLatestData() {
const interval = document.getElementById('interval').value;
// 计算最近时间范围
const endTime = new Date();
const startTime = new Date(endTime);
startTime.setHours(0, 0, 0, 0); // 设置为当天0点
// 确保时间格式符合后端要求
const startDateTime = startTime.toISOString();
const endDateTime = endTime.toISOString();
// 加载状态指示
document.getElementById('mainChart').style.opacity = 0.5;
// 首先触发设备查询并获取最新数据
triggerQueryAndFetchData()
.then(() => {
// 获取聚合数据
return fetch(`/api/latest?interval=${interval}&start=${startDateTime}&end=${endDateTime}`);
})
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('后端错误:', data.error);
alert('获取数据失败: ' + data.error);
document.getElementById('mainChart').style.opacity = 1;
return;
}
// 检查数据是否为空数组
if (!data || !Array.isArray(data) || data.length === 0) {
console.log('没有可用的数据');
alert('没有可用的数据,请稍后再试');
document.getElementById('mainChart').style.opacity = 1;
return;
}
updateChart(data);
updateTable(data);
// 恢复正常显示
document.getElementById('mainChart').style.opacity = 1;
// 自动更新日期选择器为最近查询的时间范围
document.getElementById('startDate').value = formatDateTime(startTime);
document.getElementById('endDate').value = formatDateTime(endTime);
})
.catch(error => {
console.error('Error:', error);
alert('获取最新数据失败,请检查网络连接');
// 恢复正常显示
document.getElementById('mainChart').style.opacity = 1;
});
}
// 加载历史数据(不触发设备查询)
function loadInitialData() {
const interval = document.getElementById('interval').value;
// 计算最近时间范围
const endTime = new Date();
const startTime = new Date(endTime);
startTime.setHours(0, 0, 0, 0); // 设置为当天0点
// 确保时间格式符合后端要求
const startDateTime = startTime.toISOString();
const endDateTime = endTime.toISOString();
// 获取聚合数据
fetch(`/api/latest?interval=${interval}&start=${startDateTime}&end=${endDateTime}`)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('后端错误:', data.error);
console.log('获取历史数据失败,将尝试获取最新传感器数据');
fetchLatestSensorData(); // 至少尝试获取最新传感器数据
return;
}
// 检查数据是否为空数组
if (!data || !Array.isArray(data) || data.length === 0) {
console.log('没有可用的历史数据,将尝试获取最新传感器数据');
fetchLatestSensorData(); // 至少尝试获取最新传感器数据
return;
}
updateChart(data);
updateTable(data);
// 自动更新日期选择器为最近查询的时间范围
document.getElementById('startDate').value = formatDateTime(startTime);
document.getElementById('endDate').value = formatDateTime(endTime);
})
.catch(error => {
console.error('Error:', error);
console.log('获取历史数据失败,将尝试获取最新传感器数据');
fetchLatestSensorData(); // 至少尝试获取最新传感器数据
});
}
// 更新图表
function updateChart(data) {
const ctx = document.getElementById('mainChart').getContext('2d');
if (mainChart) {
mainChart.destroy();
}
// 检查数据是否有效
if (!data || !Array.isArray(data) || data.length === 0) {
console.log('没有可用的图表数据');
return;
}
try {
// 按时间正序排列数据(从早到晚)
data.sort((a, b) => {
const timeA = a.formatted_time ? new Date(a.formatted_time) : new Date(a.timestamp);
const timeB = b.formatted_time ? new Date(b.formatted_time) : new Date(b.timestamp);
return timeA - timeB;
});
const labels = data.map(item => {
// 优先使用formatted_time如果不存在则尝试使用timestamp
let timeStr = item.formatted_time || item.timestamp;
try {
// 解析时间字符串为本地时间
const date = new Date(timeStr);
// 检查日期是否有效
if (isNaN(date.getTime())) {
return timeStr; // 如果无法解析,直接返回原始字符串
}
// 格式化为中文日期时间格式
return 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');
} catch (e) {
console.error('图表时间解析错误:', e);
return timeStr; // 出错时返回原始字符串
}
});
// 是否使用自适应范围
const useAdaptiveScale = 1;
// 计算降雨量和温度的范围
let rainfallMin = 0;
let rainfallMax = 50;
let tempMin = -10;
let tempMax = 40;
if (useAdaptiveScale && data.length > 0) {
// 找出温度的最小值和最大值,并添加一些边距
const temps = data.map(item => item.avg_temperature);
tempMin = Math.floor(Math.min(...temps) - 5);
tempMax = Math.ceil(Math.max(...temps) + 5);
// 找出降雨量的最大值,并添加一些边距
const rainfalls = data.map(item => item.rainfall);
rainfallMax = Math.ceil(Math.max(...rainfalls) * 1.2) || 10; // 如果最大值是0则默认为10
}
mainChart = new Chart(ctx, {
data: {
labels: labels,
datasets: [
{
type: 'bar',
label: '小时降雨量(mm)',
data: data.map(item => item.rainfall !== null && !isNaN(item.rainfall) ? item.rainfall : 0),
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
yAxisID: 'y-rainfall',
},
{
type: 'line',
label: '温度(℃)',
data: data.map(item => item.avg_temperature !== null && !isNaN(item.avg_temperature) ? item.avg_temperature : null),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
tension: 0.1,
yAxisID: 'y-temp',
}
]
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
'y-rainfall': {
type: 'linear',
position: 'left',
title: {
display: true,
text: '小时降雨量(mm)'
},
grid: {
drawOnChartArea: false
},
min: rainfallMin,
max: rainfallMax,
suggestedMax: Math.min(10, rainfallMax)
},
'y-temp': {
type: 'linear',
position: 'right',
title: {
display: true,
text: '温度(℃)'
},
min: tempMin,
max: tempMax,
suggestedMin: Math.max(0, tempMin),
suggestedMax: Math.min(30, tempMax)
}
}
}
});
} catch (error) {
console.error('更新图表失败:', error);
alert('更新图表失败,请检查数据格式');
}
}
// 更新表格
function updateTable(data) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
// 按时间倒序排列数据(从晚到早),这样最新的数据在表格顶部
const sortedData = [...data].sort((a, b) => {
// 使用formatted_time进行排序如果不存在则尝试使用timestamp
const timeA = a.formatted_time ? new Date(a.formatted_time) : new Date(a.timestamp);
const timeB = b.formatted_time ? new Date(b.formatted_time) : new Date(b.timestamp);
return timeB - timeA;
});
sortedData.forEach(item => {
const row = document.createElement('tr');
// 优先使用formatted_time如果不存在则尝试使用timestamp
let formattedDate;
if (item.formatted_time) {
// 如果已经有格式化好的时间,直接使用
formattedDate = item.formatted_time;
} else {
// 否则尝试解析timestamp并格式化
try {
const date = new Date(item.timestamp);
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');
} catch (e) {
console.error('时间解析错误:', e);
formattedDate = '时间格式错误';
}
}
row.innerHTML = `
<td>${formattedDate}</td>
<td>${item.rainfall !== null && !isNaN(item.rainfall) ? item.rainfall.toFixed(1) : '0.0'}</td>
<td>${item.avg_temperature !== null && !isNaN(item.avg_temperature) ? item.avg_temperature.toFixed(1) : '0.0'}</td>
<td>${item.avg_humidity !== null && !isNaN(item.avg_humidity) ? item.avg_humidity.toFixed(1) : '0.0'}</td>
<td>${item.avg_wind_speed !== null && !isNaN(item.avg_wind_speed) ? item.avg_wind_speed.toFixed(2) : '0.00'}</td>
<td>${item.atm_pressure !== null && !isNaN(item.atm_pressure) ? item.atm_pressure.toFixed(1) : '0.0'}</td>
<td>${item.solar_radiation !== null && !isNaN(item.solar_radiation) ? item.solar_radiation.toFixed(0) : '0'}</td>
`;
tbody.appendChild(row);
});
}
// 导出数据
function exportData() {
// 从表格中获取完整数据
const tableRows = document.querySelectorAll('#tableBody tr');
let csv = '时间,小时降雨量(mm),温度(℃),湿度(%),风速(m/s),大气压(kPa),太阳辐射(W/m²)\n';
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
const rowData = [
cells[0].textContent, // 时间
cells[1].textContent, // 小时降雨量
cells[2].textContent, // 温度
cells[3].textContent, // 湿度
cells[4].textContent, // 风速
cells[5].textContent, // 大气压
cells[6].textContent // 太阳辐射
];
csv += rowData.join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = '雨量监测数据.csv';
link.click();
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initDatePickers();
loadInitialData();
fetchLatestSensorData();
// 每30秒检查一次连接状态
checkConnectionStatus();
connectionCheckTimer = setInterval(checkConnectionStatus, 30000);
// 每分钟自动刷新最新传感器数据(不触发设备查询)
setInterval(fetchLatestSensorData, 60000);
});
// 页面卸载时清除定时器
window.addEventListener('beforeunload', function() {
if (connectionCheckTimer) {
clearInterval(connectionCheckTimer);
}
});
</script>
</body>
</html>