feat: 优化前端页面架构

This commit is contained in:
yarnom 2025-08-23 20:04:47 +08:00
parent 67ba5cf21c
commit 29a3e9305b
6 changed files with 1052 additions and 1083 deletions

257
static/js/app.js Normal file
View File

@ -0,0 +1,257 @@
// 应用主控制器
const WeatherApp = {
cachedHistoryData: [],
cachedForecastData: [],
currentPage: 1,
itemsPerPage: 10,
filteredDevices: [],
init() {
// 初始化日期
WeatherUtils.initializeDateInputs();
// 初始化地图
WeatherMap.init(window.TIANDITU_KEY || '');
WeatherMap.loadStations();
// 定时刷新在线设备数
setInterval(() => this.updateOnlineDevices(), 30000);
// 绑定 UI
this.bindUI();
// 监听地图请求查询事件
window.addEventListener('query-history-data', () => this.queryHistoryData());
},
bindUI() {
const stationInput = document.getElementById('stationInput');
if (stationInput) {
stationInput.addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');
});
stationInput.addEventListener('change', function() {
const value = this.value.trim();
if (!value) return;
if (/^[0-9A-F]+$/i.test(value)) {
if (value.length <= 6) this.value = WeatherUtils.hexToDecimal(value);
} else {
const num = parseInt(value);
if (!isNaN(num)) this.value = num.toString();
}
});
}
const showDeviceListBtn = document.getElementById('showDeviceList');
const modal = document.getElementById('deviceModal');
const closeBtn = document.querySelector('.close-modal');
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
if (showDeviceListBtn && modal) {
showDeviceListBtn.addEventListener('click', (e) => {
e.preventDefault();
modal.style.display = 'block';
this.updateDeviceList(1);
});
}
if (prevPageBtn) {
prevPageBtn.addEventListener('click', () => {
if (this.currentPage > 1) this.updateDeviceList(this.currentPage - 1);
});
}
if (nextPageBtn) {
nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(this.filteredDevices.length / this.itemsPerPage);
if (this.currentPage < totalPages) this.updateDeviceList(this.currentPage + 1);
});
}
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => modal.style.display = 'none');
window.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; });
}
const deviceListEl = document.getElementById('deviceList');
if (deviceListEl && modal) {
deviceListEl.addEventListener('click', (e) => {
const deviceItem = e.target.closest('.device-item');
if (!deviceItem) return;
const decimalId = deviceItem.getAttribute('data-decimal-id');
const input = document.getElementById('stationInput');
if (input) input.value = decimalId;
modal.style.display = 'none';
this.queryHistoryData();
});
}
const showPastForecast = document.getElementById('showPastForecast');
if (showPastForecast) {
showPastForecast.addEventListener('change', () => {
WeatherTable.display(this.cachedHistoryData, this.cachedForecastData);
});
}
// 提供全局函数以兼容现有 HTML on* 绑定
window.switchLayer = (type) => WeatherMap.switchLayer(type);
window.toggleMap = () => WeatherMap.toggleMap();
window.queryHistoryData = () => this.queryHistoryData();
},
// 更新设备列表
updateDeviceList(page = 1) {
const deviceListContainer = document.getElementById('deviceList');
if (!deviceListContainer) return;
deviceListContainer.innerHTML = '';
this.filteredDevices = (WeatherMap.stations || [])
.filter(station => station.device_type === 'WH65LP')
.sort((a, b) => {
const aOnline = WeatherUtils.isDeviceOnline(a.last_update);
const bOnline = WeatherUtils.isDeviceOnline(b.last_update);
if (aOnline === bOnline) return 0;
return aOnline ? -1 : 1;
});
const totalPages = Math.ceil(this.filteredDevices.length / this.itemsPerPage) || 1;
this.currentPage = Math.min(Math.max(1, page), totalPages);
const currentPageEl = document.getElementById('currentPage');
const totalPagesEl = document.getElementById('totalPages');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (currentPageEl) currentPageEl.textContent = this.currentPage;
if (totalPagesEl) totalPagesEl.textContent = totalPages;
if (prevBtn) prevBtn.disabled = this.currentPage <= 1;
if (nextBtn) nextBtn.disabled = this.currentPage >= totalPages;
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const currentDevices = this.filteredDevices.slice(startIndex, endIndex);
currentDevices.forEach(device => {
const isOnline = WeatherUtils.isDeviceOnline(device.last_update);
const deviceItem = document.createElement('div');
deviceItem.className = 'device-item';
deviceItem.setAttribute('data-decimal-id', device.decimal_id);
deviceItem.innerHTML = `
<div style="font-size: 13px; color: #444">
${device.decimal_id} | ${device.name} | ${device.location || '未知位置'}
</div>
<span style="color: ${isOnline ? '#28a745' : '#dc3545'}; font-size: 12px; padding: 2px 6px; background: ${isOnline ? '#f0f9f1' : '#fef5f5'}; border-radius: 3px">${isOnline ? '在线' : '离线'}</span>
`;
deviceListContainer.appendChild(deviceItem);
});
if (this.filteredDevices.length === 0) {
deviceListContainer.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">暂无WH65LP设备</div>';
}
},
async updateOnlineDevices() {
try {
const response = await fetch('/api/system/status');
const data = await response.json();
const onlineEl = document.getElementById('onlineDevices');
if (onlineEl) onlineEl.textContent = data.online_devices;
} catch (error) {
console.error('更新在线设备数量失败:', error);
}
},
async queryHistoryData() {
const decimalId = (document.getElementById('stationInput')?.value || '').trim();
if (!decimalId) {
alert('请输入站点编号');
return;
}
if (!/^\d+$/.test(decimalId)) {
alert('请输入有效的十进制编号');
return;
}
const startTime = document.getElementById('startDate').value;
const endTime = document.getElementById('endDate').value;
const interval = document.getElementById('interval').value;
const forecastProvider = document.getElementById('forecastProvider').value;
if (!startTime || !endTime) {
alert('请选择开始和结束时间');
return;
}
try {
const historyParams = new URLSearchParams({
decimal_id: decimalId,
start_time: startTime.replace('T', ' ') + ':00',
end_time: endTime.replace('T', ' ') + ':00',
interval: interval
});
const historyResponse = await fetch(`/api/data?${historyParams}`);
if (!historyResponse.ok) throw new Error('查询历史数据失败');
const responseData = await historyResponse.json();
const historyData = Array.isArray(responseData) ? responseData : [];
let forecastData = [];
if (forecastProvider && interval === '1hour') {
try {
const hexID = WeatherUtils.decimalToHex(decimalId);
const stationID = `RS485-${hexID}`;
const forecastParams = new URLSearchParams({
station_id: stationID,
from: startTime.replace('T', ' ') + ':00',
to: endTime.replace('T', ' ') + ':00',
provider: forecastProvider
});
const forecastResponse = await fetch(`/api/forecast?${forecastParams}`);
if (forecastResponse.ok) {
const responseData = await forecastResponse.json();
forecastData = Array.isArray(responseData) ? responseData : [];
console.log(`查询到 ${forecastData.length} 条预报数据`);
}
} catch (e) {
console.warn('查询预报数据失败:', e);
}
}
this.cachedHistoryData = historyData;
this.cachedForecastData = forecastData;
if (historyData.length === 0 && forecastData.length === 0) {
alert('该时间段内无数据');
return;
}
const station = (WeatherMap.stations || []).find(s => s.decimal_id == decimalId);
const stationInfoTitle = document.getElementById('stationInfoTitle');
if (stationInfoTitle) {
if (station) {
stationInfoTitle.innerHTML = `
<strong>
${station.location || '未知位置'} ·
编号 ${decimalId} ·
坐标 ${station.latitude ? station.latitude.toFixed(6) : '未知'}, ${station.longitude ? station.longitude.toFixed(6) : '未知'}
</strong>
`;
} else {
stationInfoTitle.innerHTML = `编号 ${decimalId}`;
}
}
if (!WeatherMap.isMapCollapsed) WeatherMap.toggleMap();
WeatherChart.display(historyData, forecastData);
WeatherTable.display(historyData, forecastData);
const chartContainer = document.getElementById('chartContainer');
const tableContainer = document.getElementById('tableContainer');
if (chartContainer) chartContainer.classList.add('show');
if (tableContainer) tableContainer.classList.add('show');
setTimeout(() => {
chartContainer?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 300);
} catch (error) {
console.error('查询数据失败:', error);
alert('查询数据失败: ' + error.message);
}
}
};
window.WeatherApp = WeatherApp;
document.addEventListener('DOMContentLoaded', () => {
WeatherApp.init();
});

49
static/js/utils.js Normal file
View File

@ -0,0 +1,49 @@
// 工具函数
const WeatherUtils = {
// 格式化日期时间
formatDatetimeLocal(date) {
const offset = date.getTimezoneOffset();
const localDate = new Date(date.getTime() - offset * 60 * 1000);
return localDate.toISOString().slice(0, 16);
},
// 十六进制转十进制
hexToDecimal(hex) {
return parseInt(hex, 16).toString();
},
// 十进制转十六进制保持6位不足补0
decimalToHex(decimal) {
const hex = parseInt(decimal).toString(16).toUpperCase();
return '0'.repeat(Math.max(0, 6 - hex.length)) + hex;
},
// 检查是否为十六进制字符串
isHexString(str) {
return /^[0-9A-F]+$/i.test(str);
},
// 格式化数字保留2位小数
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '-';
return Number(value).toFixed(decimals);
},
// 检查设备是否在线
isDeviceOnline(lastUpdate) {
return new Date(lastUpdate) > new Date(Date.now() - 5 * 60 * 1000);
},
// 初始化日期输入
initializeDateInputs() {
const now = new Date();
const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 过去24小时
const endDate = new Date(now.getTime() + 3 * 60 * 60 * 1000); // 未来3小时
document.getElementById('startDate').value = this.formatDatetimeLocal(startDate);
document.getElementById('endDate').value = this.formatDatetimeLocal(endDate);
}
};
// 导出工具对象
window.WeatherUtils = WeatherUtils;

444
static/js/weather-app.js Normal file
View File

@ -0,0 +1,444 @@
// 地图相关功能
const WeatherMap = {
map: null,
stations: [],
stationLayer: null,
clusterLayer: null,
clusterSource: null,
singleStationLayer: null,
combinedChart: null,
CLUSTER_THRESHOLD: 10,
isMapCollapsed: false,
// 初始化地图
init(tiandituKey) {
this.initializeLayers();
this.initializeMap(tiandituKey);
this.setupEventListeners();
},
// 初始化图层
initializeLayers() {
this.stationLayer = new ol.layer.Vector({
source: new ol.source.Vector()
});
this.clusterSource = new ol.source.Cluster({
distance: 60,
minDistance: 20,
source: this.stationLayer.getSource(),
geometryFunction: function(feature) {
return feature.getGeometry();
}
});
this.clusterLayer = new ol.layer.Vector({
source: this.clusterSource,
style: (feature) => this.createClusterStyle(feature)
});
this.singleStationLayer = new ol.layer.Vector({
source: this.stationLayer.getSource(),
style: (feature) => this.createStationStyle(feature),
visible: false
});
},
// 初始化地图
initializeMap(tiandituKey) {
const layers = this.createMapLayers(tiandituKey);
this.layers = layers;
this.map = new ol.Map({
target: 'map',
layers: [
layers.satellite,
layers.vector,
layers.terrain,
layers.hybrid,
this.clusterLayer,
this.singleStationLayer
],
view: new ol.View({
center: ol.proj.fromLonLat([108, 35]),
zoom: 5,
minZoom: 3,
maxZoom: 18
})
});
// 初始化时设置图层状态
const initialZoom = this.map.getView().getZoom();
this.updateClusterDistance(initialZoom);
this.updateLayerVisibility(initialZoom);
},
// 创建地图图层
createMapLayers(tiandituKey) {
return {
satellite: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
]
}),
vector: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
}),
terrain: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/ter_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ter&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cta_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cta&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
}),
hybrid: new ol.layer.Group({
layers: [
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
}),
new ol.layer.Tile({
source: new ol.source.XYZ({
url: `https://t{0-7}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandituKey}`
})
})
],
visible: false
})
};
},
// 设置事件监听
setupEventListeners() {
// 监听缩放事件
this.map.getView().on('change:resolution', () => {
const zoom = this.map.getView().getZoom();
this.updateClusterDistance(zoom);
this.updateLayerVisibility(zoom);
});
// 添加点击事件
this.map.on('click', (event) => this.handleMapClick(event));
// 添加鼠标移动事件
this.map.on('pointermove', (event) => {
const pixel = this.map.getEventPixel(event.originalEvent);
const hit = this.map.hasFeatureAtPixel(pixel);
this.map.getTargetElement().style.cursor = hit ? 'pointer' : '';
});
},
// 处理地图点击
handleMapClick(event) {
const feature = this.map.forEachFeatureAtPixel(event.pixel, feature => feature);
if (!feature) return;
const features = feature.get('features');
if (features && features.length > 1) {
this.handleClusterClick(features);
} else {
this.handleStationClick(features ? features[0] : feature);
}
},
// 处理集群点击
handleClusterClick(features) {
const extent = ol.extent.createEmpty();
features.forEach(feature => {
ol.extent.extend(extent, feature.getGeometry().getExtent());
});
const zoom = this.map.getView().getZoom();
const targetZoom = Math.min(
features.length <= 5 ? this.CLUSTER_THRESHOLD : zoom + 2,
this.CLUSTER_THRESHOLD
);
this.map.getView().fit(extent, {
padding: [100, 100, 100, 100],
duration: 800,
maxZoom: targetZoom
});
},
// 处理站点点击
handleStationClick(feature) {
const decimalId = feature.get('decimalId');
if (!decimalId) return;
document.getElementById('stationInput').value = decimalId;
const center = feature.getGeometry().getCoordinates();
const currentZoom = this.map.getView().getZoom();
if (currentZoom < this.CLUSTER_THRESHOLD) {
this.map.getView().animate({
center: center,
zoom: this.CLUSTER_THRESHOLD,
duration: 500
});
}
if (document.getElementById('interval').value === '1hour') {
// 触发查询事件
window.dispatchEvent(new CustomEvent('query-history-data'));
}
},
// 更新集群距离
updateClusterDistance(zoom) {
let distance;
if (zoom < 5) distance = 120;
else if (zoom < 7) distance = 90;
else if (zoom < 9) distance = 60;
else if (zoom < this.CLUSTER_THRESHOLD) distance = 40;
else distance = 0;
this.clusterSource.setDistance(distance);
this.clusterSource.refresh();
setTimeout(() => {
this.clusterLayer.changed();
this.singleStationLayer.changed();
}, 100);
},
// 更新图层可见性
updateLayerVisibility(zoom) {
if (zoom >= this.CLUSTER_THRESHOLD) {
this.clusterLayer.setVisible(false);
this.singleStationLayer.setVisible(true);
} else {
this.clusterLayer.setVisible(true);
this.singleStationLayer.setVisible(false);
}
},
// 创建站点样式
createStationStyle(feature) {
const isOnline = new Date(feature.get('lastUpdate')) > new Date(Date.now() - 5*60*1000);
const zoom = this.map ? this.map.getView().getZoom() : 10;
let labelText = '';
if (zoom >= this.CLUSTER_THRESHOLD - 2) {
labelText = `${feature.get('decimalId') || '未知'} | ${feature.get('name') || '未知'} | ${feature.get('location') || '未知'}`;
}
return new ol.style.Style({
image: this.getMarkerIconStyle(isOnline).getImage(),
text: new ol.style.Text({
text: labelText,
font: '11px Arial',
offsetY: -24,
textAlign: 'center',
textBaseline: 'bottom',
fill: new ol.style.Fill({ color: '#666' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
})
});
},
// 创建集群样式
createClusterStyle(feature) {
const features = feature.get('features');
const size = features.length;
const zoom = this.map.getView().getZoom();
if (zoom < this.CLUSTER_THRESHOLD) {
if (size > 1) {
const radius = Math.min(16 + size * 0.8, 32);
const fontSize = Math.min(11 + size/12, 16);
return new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({ color: 'rgba(0, 123, 255, 0.8)' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
}),
text: new ol.style.Text({
text: String(size),
fill: new ol.style.Fill({ color: '#fff' }),
font: `bold ${fontSize}px Arial`,
offsetY: 1
})
});
}
return new ol.style.Style({
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({
color: new Date(features[0].get('lastUpdate')) >
new Date(Date.now() - 5*60*1000) ? '#007bff' : '#6c757d'
}),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
})
});
}
if (size === 1) {
return this.createStationStyle(features[0]);
}
const radius = Math.min(16 + size * 0.8, 32);
const fontSize = Math.min(11 + size/12, 16);
return new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({ color: 'rgba(0, 123, 255, 0.8)' }),
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
}),
text: new ol.style.Text({
text: String(size),
fill: new ol.style.Fill({ color: '#fff' }),
font: `bold ${fontSize}px Arial`,
offsetY: 1
})
});
},
// 获取标记图标样式
getMarkerIconStyle(isOnline) {
const key = isOnline ? 'online' : 'offline';
if (this.markerStyleCache?.[key]) return this.markerStyleCache[key];
const iconPath = isOnline ? '/static/images/marker-online.svg' : '/static/images/marker-offline.svg';
const style = new ol.style.Style({
image: new ol.style.Icon({
src: iconPath,
anchor: [0.5, 1],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
scale: 0.9
})
});
if (!this.markerStyleCache) this.markerStyleCache = {};
this.markerStyleCache[key] = style;
return style;
},
// 切换地图图层
switchLayer(layerType) {
Object.keys(this.layers).forEach(key => {
this.layers[key].setVisible(key === layerType);
});
},
// 切换地图折叠状态
toggleMap() {
this.isMapCollapsed = !this.isMapCollapsed;
const mapContainer = document.getElementById('mapContainer');
const toggleBtn = document.getElementById('toggleMapBtn');
if (this.isMapCollapsed) {
mapContainer.classList.add('collapsed');
toggleBtn.textContent = '展开地图';
} else {
mapContainer.classList.remove('collapsed');
toggleBtn.textContent = '折叠地图';
}
setTimeout(() => {
this.map.updateSize();
}, 300);
},
// 加载站点数据
async loadStations() {
try {
const response = await fetch('/api/stations');
this.stations = await response.json();
// 更新WH65LP设备数量
const wh65lpDevices = this.stations.filter(station => station.device_type === 'WH65LP');
document.getElementById('wh65lpCount').textContent = wh65lpDevices.length;
this.displayStationsOnMap();
} catch (error) {
console.error('加载站点失败:', error);
}
},
// 在地图上显示站点
displayStationsOnMap() {
const source = this.stationLayer.getSource();
source.clear();
const now = Date.now();
const fiveMinutesAgo = now - 5 * 60 * 1000;
let onlineCount = 0;
let offlineCount = 0;
this.stations.forEach(station => {
if (station.latitude && station.longitude) {
const isOnline = new Date(station.last_update) > new Date(fiveMinutesAgo);
if (isOnline) onlineCount++; else offlineCount++;
const feature = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([station.longitude, station.latitude])),
stationId: station.station_id,
decimalId: station.decimal_id,
name: station.name,
location: station.location,
lastUpdate: station.last_update,
isOnline: isOnline
});
source.addFeature(feature);
}
});
console.log(`已加载 ${this.stations.length} 个站点,在线: ${onlineCount},离线: ${offlineCount}`);
if (source.getFeatures().length > 0) {
if (source.getFeatures().length === 1) {
const feature = source.getFeatures()[0];
this.map.getView().setCenter(feature.getGeometry().getCoordinates());
this.map.getView().setZoom(12);
} else {
const extent = source.getExtent();
this.map.getView().fit(extent, {
padding: [50, 50, 50, 50],
maxZoom: 10
});
}
}
this.updateClusterDistance(this.map.getView().getZoom());
}
};
// 导出地图对象
window.WeatherMap = WeatherMap;

163
static/js/weather-chart.js Normal file
View File

@ -0,0 +1,163 @@
// 图表相关功能
const WeatherChart = {
chart: null,
// 显示图表
display(historyData = [], forecastData = []) {
// 确保数据是数组
historyData = Array.isArray(historyData) ? historyData : [];
forecastData = Array.isArray(forecastData) ? forecastData : [];
// 如果没有任何数据,则不绘制图表
if (historyData.length === 0 && forecastData.length === 0) {
return;
}
// 合并历史数据和预报数据的时间轴
const allLabels = [...new Set([
...historyData.map(item => item.date_time),
...forecastData.map(item => item.date_time)
])].sort();
// 准备历史数据
const historyTemperatures = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.temperature : null;
});
const historyHumidities = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.humidity : null;
});
const historyRainfalls = allLabels.map(label => {
const item = historyData.find(d => d.date_time === label);
return item ? item.rainfall : null;
});
// 准备预报数据
const forecastTemperatures = allLabels.map(label => {
const item = forecastData.find(d => d.date_time === label);
return item && item.temperature !== null ? item.temperature : null;
});
const forecastRainfalls = allLabels.map(label => {
const item = forecastData.find(d => d.date_time === label);
return item && item.rainfall !== null ? item.rainfall : null;
});
// 销毁旧图表
if (this.chart) this.chart.destroy();
// 创建数据集
const datasets = [
{
label: '温度 (°C) - 实测',
data: historyTemperatures,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
yAxisID: 'y-temperature',
tension: 0.4,
spanGaps: false
},
{
label: '湿度 (%) - 实测',
data: historyHumidities,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
yAxisID: 'y-humidity',
tension: 0.4,
hidden: true,
spanGaps: false
},
{
label: '雨量 (mm) - 实测',
data: historyRainfalls,
type: 'bar',
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgb(54, 162, 235)',
yAxisID: 'y-rainfall'
}
];
// 添加预报数据集
if (forecastData.length > 0) {
datasets.push(
{
label: '温度 (°C) - 预报',
data: forecastTemperatures,
borderColor: 'rgb(255, 165, 0)',
backgroundColor: 'rgba(255, 165, 0, 0.1)',
borderDash: [5, 5],
yAxisID: 'y-temperature',
tension: 0.4,
spanGaps: false
},
{
label: '雨量 (mm) - 预报',
data: forecastRainfalls,
type: 'bar',
backgroundColor: 'rgba(255, 165, 0, 0.4)',
borderColor: 'rgb(255, 165, 0)',
yAxisID: 'y-rainfall'
}
);
}
// 创建组合图表
const ctx = document.getElementById('combinedChart').getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: allLabels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
'y-temperature': {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: '温度 (°C)'
}
},
'y-humidity': {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '湿度 (%)'
},
grid: {
drawOnChartArea: false
},
min: 0,
max: 100
},
'y-rainfall': {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: '雨量 (mm)'
},
grid: {
drawOnChartArea: false
},
beginAtZero: true
}
}
}
});
}
};
// 导出图表对象
window.WeatherChart = WeatherChart;

103
static/js/weather-table.js Normal file
View File

@ -0,0 +1,103 @@
// 表格渲染功能
const WeatherTable = {
display(historyData = [], forecastData = []) {
const tbody = document.getElementById('tableBody');
if (!tbody) return;
tbody.innerHTML = '';
historyData = Array.isArray(historyData) ? historyData : [];
forecastData = Array.isArray(forecastData) ? forecastData : [];
const nowTs = Date.now();
const future3hTs = nowTs + 3 * 60 * 60 * 1000;
const showPastForecastEl = document.getElementById('showPastForecast');
const shouldShowPast = !!(showPastForecastEl && showPastForecastEl.checked);
const displayedForecast = forecastData.filter(item => {
const t = new Date(item.date_time).getTime();
const isFuture3h = t > nowTs && t <= future3hTs;
const isPast = t <= nowTs;
return isFuture3h || (shouldShowPast && isPast);
});
const hasForecast = displayedForecast.length > 0;
const forecastToggleContainer = document.getElementById('forecastToggleContainer');
if (forecastToggleContainer) {
forecastToggleContainer.style.display = forecastData.length > 0 ? 'block' : 'none';
}
const thead = document.getElementById('tableHeader');
if (thead) {
thead.innerHTML = '';
const fixedHeaders = ['时间', '温度 (°C)', '湿度 (%)', '气压 (hPa)', '风速 (m/s)', '风向 (°)', '雨量 (mm)'];
fixedHeaders.forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.className = 'bg-gray-50 font-semibold';
thead.appendChild(th);
});
if (hasForecast) {
const th = document.createElement('th');
th.textContent = '降水概率 (%)';
th.className = 'bg-gray-50 font-semibold';
thead.appendChild(th);
}
const remainingHeaders = ['光照 (lux)', '紫外线'];
remainingHeaders.forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.className = 'bg-gray-50 font-semibold';
thead.appendChild(th);
});
}
const allData = [];
historyData.forEach(item => {
allData.push({ ...item, source: '实测' });
});
displayedForecast.forEach(item => {
allData.push({
...item,
source: '预报',
light: null,
wind_speed: item.wind_speed !== null ? item.wind_speed : 0,
wind_direction: item.wind_direction !== null ? item.wind_direction : 0
});
});
allData.sort((a, b) => new Date(b.date_time) - new Date(a.date_time));
const fmt2 = v => (v === null || v === undefined ? '-' : Number(v).toFixed(2));
const fmt3 = v => (v === null || v === undefined ? '-' : Number(v).toFixed(3));
allData.forEach(item => {
const row = document.createElement('tr');
if (item.source === '预报') {
row.style.backgroundColor = 'rgba(255, 165, 0, 0.08)';
}
const columns = [
`<td>${item.date_time}${hasForecast ? ` <span style="font-size: 12px; color: ${item.source === '预报' ? '#ff8c00' : '#28a745'};">[${item.source}]</span>` : ''}</td>`,
`<td>${fmt2(item.temperature)}</td>`,
`<td>${fmt2(item.humidity)}</td>`,
`<td>${fmt2(item.pressure)}</td>`,
`<td>${fmt2(item.wind_speed)}</td>`,
`<td>${fmt2(item.wind_direction)}</td>`,
`<td>${fmt3(item.rainfall)}</td>`
];
if (hasForecast) {
columns.push(`<td>${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? item.precip_prob : '-'}</td>`);
}
columns.push(
`<td>${(item.light !== null && item.light !== undefined) ? Number(item.light).toFixed(2) : '-'}</td>`,
`<td>${(item.uv !== null && item.uv !== undefined) ? Number(item.uv).toFixed(2) : '-'}</td>`
);
row.innerHTML = columns.join('');
tbody.appendChild(row);
});
}
};
window.WeatherTable = WeatherTable;

File diff suppressed because it is too large Load Diff