feat: 优化前端页面
This commit is contained in:
parent
d26c2c025a
commit
9cd26d3df3
@ -1,4 +1,3 @@
|
|||||||
// 应用主控制器
|
|
||||||
const WeatherApp = {
|
const WeatherApp = {
|
||||||
cachedHistoryData: [],
|
cachedHistoryData: [],
|
||||||
cachedForecastData: [],
|
cachedForecastData: [],
|
||||||
@ -7,16 +6,11 @@ const WeatherApp = {
|
|||||||
filteredDevices: [],
|
filteredDevices: [],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// 初始化日期
|
|
||||||
WeatherUtils.initializeDateInputs();
|
WeatherUtils.initializeDateInputs();
|
||||||
// 初始化地图
|
|
||||||
WeatherMap.init(window.TIANDITU_KEY || '');
|
WeatherMap.init(window.TIANDITU_KEY || '');
|
||||||
WeatherMap.loadStations();
|
WeatherMap.loadStations();
|
||||||
// 定时刷新在线设备数
|
|
||||||
setInterval(() => this.updateOnlineDevices(), 30000);
|
setInterval(() => this.updateOnlineDevices(), 30000);
|
||||||
// 绑定 UI
|
|
||||||
this.bindUI();
|
this.bindUI();
|
||||||
// 监听地图请求查询事件
|
|
||||||
window.addEventListener('query-history-data', () => this.queryHistoryData());
|
window.addEventListener('query-history-data', () => this.queryHistoryData());
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -44,30 +38,6 @@ const WeatherApp = {
|
|||||||
const prevPageBtn = document.getElementById('prevPage');
|
const prevPageBtn = document.getElementById('prevPage');
|
||||||
const nextPageBtn = document.getElementById('nextPage');
|
const nextPageBtn = document.getElementById('nextPage');
|
||||||
|
|
||||||
// 由 Alpine 管理设备列表弹窗开关与分页,不再绑定以下事件
|
|
||||||
// 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');
|
const deviceListEl = document.getElementById('deviceList');
|
||||||
if (deviceListEl && modal) {
|
if (deviceListEl && modal) {
|
||||||
deviceListEl.addEventListener('click', (e) => {
|
deviceListEl.addEventListener('click', (e) => {
|
||||||
@ -76,7 +46,6 @@ const WeatherApp = {
|
|||||||
const decimalId = deviceItem.getAttribute('data-decimal-id');
|
const decimalId = deviceItem.getAttribute('data-decimal-id');
|
||||||
const input = document.getElementById('stationInput');
|
const input = document.getElementById('stationInput');
|
||||||
if (input) input.value = decimalId;
|
if (input) input.value = decimalId;
|
||||||
// 关闭交给 Alpine: deviceModalOpen = false
|
|
||||||
window.dispatchEvent(new CustomEvent('close-device-modal'));
|
window.dispatchEvent(new CustomEvent('close-device-modal'));
|
||||||
this.queryHistoryData();
|
this.queryHistoryData();
|
||||||
});
|
});
|
||||||
@ -89,13 +58,21 @@ const WeatherApp = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提供全局函数以兼容现有 HTML on* 绑定
|
const legendMode = document.getElementById('legendMode');
|
||||||
|
if (legendMode) {
|
||||||
|
legendMode.addEventListener('change', (e) => {
|
||||||
|
const mode = e.target.value;
|
||||||
|
if (window.WeatherChart && typeof window.WeatherChart.applyLegendMode === 'function') {
|
||||||
|
window.WeatherChart.applyLegendMode(mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.switchLayer = (type) => WeatherMap.switchLayer(type);
|
window.switchLayer = (type) => WeatherMap.switchLayer(type);
|
||||||
window.toggleMap = () => WeatherMap.toggleMap();
|
window.toggleMap = () => WeatherMap.toggleMap();
|
||||||
window.queryHistoryData = () => this.queryHistoryData();
|
window.queryHistoryData = () => this.queryHistoryData();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新设备列表
|
|
||||||
updateDeviceList(page = 1) {
|
updateDeviceList(page = 1) {
|
||||||
const deviceListContainer = document.getElementById('deviceList');
|
const deviceListContainer = document.getElementById('deviceList');
|
||||||
if (!deviceListContainer) return;
|
if (!deviceListContainer) return;
|
||||||
@ -247,6 +224,11 @@ const WeatherApp = {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chartContainer?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
chartContainer?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
const legendMode = document.getElementById('legendMode');
|
||||||
|
if (legendMode) {
|
||||||
|
WeatherChart.applyLegendMode(legendMode.value);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('查询数据失败:', error);
|
console.error('查询数据失败:', error);
|
||||||
alert('查询数据失败: ' + error.message);
|
alert('查询数据失败: ' + error.message);
|
||||||
|
|||||||
@ -1,49 +1,39 @@
|
|||||||
// 工具函数
|
|
||||||
const WeatherUtils = {
|
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() {
|
initializeDateInputs() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 过去24小时
|
const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
const endDate = new Date(now.getTime() + 3 * 60 * 60 * 1000); // 未来3小时
|
const endDate = new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
||||||
|
|
||||||
document.getElementById('startDate').value = this.formatDatetimeLocal(startDate);
|
const startDateInput = document.getElementById('startDate');
|
||||||
document.getElementById('endDate').value = this.formatDatetimeLocal(endDate);
|
const endDateInput = document.getElementById('endDate');
|
||||||
|
|
||||||
|
if (startDateInput) {
|
||||||
|
startDateInput.value = startDate.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
if (endDateInput) {
|
||||||
|
endDateInput.value = endDate.toISOString().slice(0, 16);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isDeviceOnline(lastUpdate) {
|
||||||
|
if (!lastUpdate) return false;
|
||||||
|
const lastUpdateTime = new Date(lastUpdate);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMinutes = (now - lastUpdateTime) / (1000 * 60);
|
||||||
|
return diffMinutes <= 5;
|
||||||
|
},
|
||||||
|
|
||||||
|
decimalToHex(decimal) {
|
||||||
|
const num = parseInt(decimal);
|
||||||
|
if (isNaN(num)) return '';
|
||||||
|
return num.toString(16).toUpperCase().padStart(6, '0');
|
||||||
|
},
|
||||||
|
|
||||||
|
hexToDecimal(hex) {
|
||||||
|
const num = parseInt(hex, 16);
|
||||||
|
if (isNaN(num)) return '';
|
||||||
|
return num.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出工具对象
|
|
||||||
window.WeatherUtils = WeatherUtils;
|
window.WeatherUtils = WeatherUtils;
|
||||||
@ -1,25 +1,19 @@
|
|||||||
// 图表相关功能
|
|
||||||
const WeatherChart = {
|
const WeatherChart = {
|
||||||
chart: null,
|
chart: null,
|
||||||
|
|
||||||
// 显示图表
|
|
||||||
display(historyData = [], forecastData = []) {
|
display(historyData = [], forecastData = []) {
|
||||||
// 确保数据是数组
|
|
||||||
historyData = Array.isArray(historyData) ? historyData : [];
|
historyData = Array.isArray(historyData) ? historyData : [];
|
||||||
forecastData = Array.isArray(forecastData) ? forecastData : [];
|
forecastData = Array.isArray(forecastData) ? forecastData : [];
|
||||||
|
|
||||||
// 如果没有任何数据,则不绘制图表
|
|
||||||
if (historyData.length === 0 && forecastData.length === 0) {
|
if (historyData.length === 0 && forecastData.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并历史数据和预报数据的时间轴(按 date_time 对齐)
|
|
||||||
const allLabels = [...new Set([
|
const allLabels = [...new Set([
|
||||||
...historyData.map(item => item.date_time),
|
...historyData.map(item => item.date_time),
|
||||||
...forecastData.map(item => item.date_time)
|
...forecastData.map(item => item.date_time)
|
||||||
])].sort();
|
])].sort();
|
||||||
|
|
||||||
// 历史数据
|
|
||||||
const historyTemperatures = allLabels.map(label => {
|
const historyTemperatures = allLabels.map(label => {
|
||||||
const item = historyData.find(d => d.date_time === label);
|
const item = historyData.find(d => d.date_time === label);
|
||||||
return item ? item.temperature : null;
|
return item ? item.temperature : null;
|
||||||
@ -45,7 +39,6 @@ const WeatherChart = {
|
|||||||
return item && item.rain_total !== undefined ? item.rain_total : null;
|
return item && item.rain_total !== undefined ? item.rain_total : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 预报:按同一 forecast_time 的不同 lead_hours 组织(仅用 0..3h)
|
|
||||||
const byTime = new Map();
|
const byTime = new Map();
|
||||||
forecastData.forEach(fp => {
|
forecastData.forEach(fp => {
|
||||||
if (!byTime.has(fp.date_time)) byTime.set(fp.date_time, {});
|
if (!byTime.has(fp.date_time)) byTime.set(fp.date_time, {});
|
||||||
@ -73,7 +66,6 @@ const WeatherChart = {
|
|||||||
const forecastRainfallsH3 = allLabels.map(label => getRainAtLead(label, 3));
|
const forecastRainfallsH3 = allLabels.map(label => getRainAtLead(label, 3));
|
||||||
|
|
||||||
const pickNearest = (label, field) => {
|
const pickNearest = (label, field) => {
|
||||||
// 近似优先级:0h > 1h > 2h > 3h
|
|
||||||
const b = byTime.get(label);
|
const b = byTime.get(label);
|
||||||
if (!b) return null;
|
if (!b) return null;
|
||||||
if (b[0] && b[0][field] != null) return b[0][field];
|
if (b[0] && b[0][field] != null) return b[0][field];
|
||||||
@ -87,13 +79,12 @@ const WeatherChart = {
|
|||||||
const forecastPressuresNearest = allLabels.map(label => pickNearest(label, 'pressure'));
|
const forecastPressuresNearest = allLabels.map(label => pickNearest(label, 'pressure'));
|
||||||
const forecastWindSpeedsNearest = allLabels.map(label => pickNearest(label, 'wind_speed'));
|
const forecastWindSpeedsNearest = allLabels.map(label => pickNearest(label, 'wind_speed'));
|
||||||
|
|
||||||
// 销毁旧图表
|
|
||||||
if (this.chart) this.chart.destroy();
|
if (this.chart) this.chart.destroy();
|
||||||
|
|
||||||
// 数据集
|
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: '温度 (°C) - 实测',
|
label: '温度 (°C) - 实测',
|
||||||
|
seriesKey: 'temp_actual',
|
||||||
data: historyTemperatures,
|
data: historyTemperatures,
|
||||||
borderColor: 'rgb(255, 99, 132)',
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
@ -103,6 +94,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '湿度 (%) - 实测',
|
label: '湿度 (%) - 实测',
|
||||||
|
seriesKey: 'hum_actual',
|
||||||
data: historyHumidities,
|
data: historyHumidities,
|
||||||
borderColor: 'rgb(54, 162, 235)',
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||||
@ -113,6 +105,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '大气压 (hPa) - 实测',
|
label: '大气压 (hPa) - 实测',
|
||||||
|
seriesKey: 'pressure_actual',
|
||||||
data: historyPressures,
|
data: historyPressures,
|
||||||
borderColor: 'rgb(153, 102, 255)',
|
borderColor: 'rgb(153, 102, 255)',
|
||||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||||
@ -123,6 +116,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '风速 (m/s) - 实测',
|
label: '风速 (m/s) - 实测',
|
||||||
|
seriesKey: 'wind_actual',
|
||||||
data: historyWindSpeeds,
|
data: historyWindSpeeds,
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
@ -133,6 +127,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '雨量 (mm) - 实测',
|
label: '雨量 (mm) - 实测',
|
||||||
|
seriesKey: 'rain_actual',
|
||||||
data: historyRainfalls,
|
data: historyRainfalls,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||||
@ -141,6 +136,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '累计雨量 (mm) - 实测',
|
label: '累计雨量 (mm) - 实测',
|
||||||
|
seriesKey: 'rain_total',
|
||||||
data: historyRainTotals,
|
data: historyRainTotals,
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
@ -153,27 +149,28 @@ const WeatherChart = {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (forecastData.length > 0) {
|
if (forecastData.length > 0) {
|
||||||
// 雨量 仅显示 -1h/-2h/-3h
|
|
||||||
datasets.push(
|
datasets.push(
|
||||||
{ label: '雨量 (mm) - 预报 (+1h)', data: forecastRainfallsH1, type: 'bar', backgroundColor: 'rgba(255, 99, 71, 0.55)', borderColor: 'rgb(255, 99, 71)', yAxisID: 'y-rainfall' },
|
{ label: '雨量 (mm) - 预报 (+1h)', seriesKey: 'rain_fcst_h1', data: forecastRainfallsH1, type: 'bar', backgroundColor: 'rgba(255, 99, 71, 0.55)', borderColor: 'rgb(255, 99, 71)', yAxisID: 'y-rainfall' },
|
||||||
{ label: '雨量 (mm) - 预报 (+2h)', data: forecastRainfallsH2, type: 'bar', backgroundColor: 'rgba(255, 205, 86, 0.55)', borderColor: 'rgb(255, 205, 86)', yAxisID: 'y-rainfall' },
|
{ label: '雨量 (mm) - 预报 (+2h)', seriesKey: 'rain_fcst_h2', data: forecastRainfallsH2, type: 'bar', backgroundColor: 'rgba(255, 205, 86, 0.55)', borderColor: 'rgb(255, 205, 86)', yAxisID: 'y-rainfall' },
|
||||||
{ label: '雨量 (mm) - 预报 (+3h)', data: forecastRainfallsH3, type: 'bar', backgroundColor: 'rgba(76, 175, 80, 0.55)', borderColor: 'rgb(76, 175, 80)', yAxisID: 'y-rainfall' }
|
{ label: '雨量 (mm) - 预报 (+3h)', seriesKey: 'rain_fcst_h3', data: forecastRainfallsH3, type: 'bar', backgroundColor: 'rgba(76, 175, 80, 0.55)', borderColor: 'rgb(76, 175, 80)', yAxisID: 'y-rainfall' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 其他预报数据(取最接近的预报:0h>1h>2h>3h)
|
|
||||||
datasets.push(
|
datasets.push(
|
||||||
{
|
{
|
||||||
label: '温度 (°C) - 预报',
|
label: '温度 (°C) - 预报',
|
||||||
|
seriesKey: 'temp_fcst',
|
||||||
data: forecastTemperaturesNearest,
|
data: forecastTemperaturesNearest,
|
||||||
borderColor: 'rgb(255, 159, 64)',
|
borderColor: 'rgb(255, 159, 64)',
|
||||||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
yAxisID: 'y-temperature',
|
yAxisID: 'y-temperature',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
spanGaps: false
|
spanGaps: false,
|
||||||
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '湿度 (%) - 预报',
|
label: '湿度 (%) - 预报',
|
||||||
|
seriesKey: 'hum_fcst',
|
||||||
data: forecastHumiditiesNearest,
|
data: forecastHumiditiesNearest,
|
||||||
borderColor: 'rgb(54, 162, 235)',
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||||
@ -185,6 +182,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '大气压 (hPa) - 预报',
|
label: '大气压 (hPa) - 预报',
|
||||||
|
seriesKey: 'pressure_fcst',
|
||||||
data: forecastPressuresNearest,
|
data: forecastPressuresNearest,
|
||||||
borderColor: 'rgb(153, 102, 255)',
|
borderColor: 'rgb(153, 102, 255)',
|
||||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||||
@ -196,6 +194,7 @@ const WeatherChart = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '风速 (m/s) - 预报',
|
label: '风速 (m/s) - 预报',
|
||||||
|
seriesKey: 'wind_fcst',
|
||||||
data: forecastWindSpeedsNearest,
|
data: forecastWindSpeedsNearest,
|
||||||
borderColor: 'rgb(75, 192, 192)',
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||||
@ -208,9 +207,8 @@ const WeatherChart = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建组合图表
|
|
||||||
const ctx = document.getElementById('combinedChart').getContext('2d');
|
const ctx = document.getElementById('combinedChart').getContext('2d');
|
||||||
this.chart = new Chart(ctx, {
|
const chartConfig = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: allLabels,
|
labels: allLabels,
|
||||||
@ -235,7 +233,7 @@ const WeatherChart = {
|
|||||||
'y-temperature': {
|
'y-temperature': {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
display: true,
|
display: true,
|
||||||
position: 'left',
|
position: 'right',
|
||||||
title: { display: true, text: '温度 (°C)' }
|
title: { display: true, text: '温度 (°C)' }
|
||||||
},
|
},
|
||||||
'y-humidity': {
|
'y-humidity': {
|
||||||
@ -250,14 +248,14 @@ const WeatherChart = {
|
|||||||
'y-pressure': {
|
'y-pressure': {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
display: true,
|
display: true,
|
||||||
position: 'right',
|
position: 'left',
|
||||||
title: { display: true, text: '大气压 (hPa)' },
|
title: { display: true, text: '大气压 (hPa)' },
|
||||||
grid: { drawOnChartArea: false }
|
grid: { drawOnChartArea: false }
|
||||||
},
|
},
|
||||||
'y-wind': {
|
'y-wind': {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
display: true,
|
display: true,
|
||||||
position: 'right',
|
position: 'left',
|
||||||
title: { display: true, text: '风速 (m/s)' },
|
title: { display: true, text: '风速 (m/s)' },
|
||||||
grid: { drawOnChartArea: false },
|
grid: { drawOnChartArea: false },
|
||||||
beginAtZero: true
|
beginAtZero: true
|
||||||
@ -272,9 +270,75 @@ const WeatherChart = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.chart = new Chart(ctx, chartConfig);
|
||||||
|
|
||||||
|
const mode = document.getElementById('legendMode')?.value || 'combo_standard';
|
||||||
|
this.applyLegendMode(mode);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出图表对象
|
WeatherChart.applyLegendMode = function(mode) {
|
||||||
|
if (!this.chart) return;
|
||||||
|
|
||||||
|
// 设置数据集可见性
|
||||||
|
const map = new Map();
|
||||||
|
this.chart.data.datasets.forEach(ds => {
|
||||||
|
if (ds.seriesKey) map.set(ds.seriesKey, ds);
|
||||||
|
});
|
||||||
|
|
||||||
|
const setVisible = (keys) => {
|
||||||
|
const allKeys = ['temp_actual','hum_actual','rain_actual','rain_total','temp_fcst','hum_fcst','pressure_actual','pressure_fcst','wind_actual','wind_fcst','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3'];
|
||||||
|
allKeys.forEach(k => { if (map.has(k)) map.get(k).hidden = true; });
|
||||||
|
keys.forEach(k => { if (map.has(k)) map.get(k).hidden = false; });
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'combo_compact':
|
||||||
|
setVisible(['temp_actual','hum_actual','rain_actual','rain_fcst_h1']);
|
||||||
|
break;
|
||||||
|
case 'verify_all':
|
||||||
|
setVisible(['temp_actual','temp_fcst','hum_actual','hum_fcst','pressure_actual','pressure_fcst','wind_actual','wind_fcst','rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
|
||||||
|
break;
|
||||||
|
case 'temp_compare':
|
||||||
|
setVisible(['temp_actual','temp_fcst']);
|
||||||
|
break;
|
||||||
|
case 'hum_compare':
|
||||||
|
setVisible(['hum_actual','hum_fcst']);
|
||||||
|
break;
|
||||||
|
case 'rain_all':
|
||||||
|
setVisible(['rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
|
||||||
|
break;
|
||||||
|
case 'pressure_compare':
|
||||||
|
setVisible(['pressure_actual','pressure_fcst']);
|
||||||
|
break;
|
||||||
|
case 'wind_compare':
|
||||||
|
setVisible(['wind_actual','wind_fcst']);
|
||||||
|
break;
|
||||||
|
case 'combo_standard':
|
||||||
|
default:
|
||||||
|
setVisible(['temp_actual','hum_actual','rain_actual','rain_fcst_h1','rain_fcst_h2','rain_fcst_h3']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.chart.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
WeatherChart.updateAxesVisibility = function() {
|
||||||
|
if (!this.chart) return;
|
||||||
|
|
||||||
|
// 检查每个轴是否有可见的数据集
|
||||||
|
const hasVisibleDatasets = (axisId) => {
|
||||||
|
return this.chart.data.datasets.some(ds => !ds.hidden && ds.yAxisID === axisId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新每个轴的显示状态
|
||||||
|
Object.entries(this.chart.options.scales).forEach(([scaleId, scale]) => {
|
||||||
|
const shouldDisplay = hasVisibleDatasets(scaleId);
|
||||||
|
if (scale.display !== shouldDisplay) {
|
||||||
|
scale.display = shouldDisplay;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
window.WeatherChart = WeatherChart;
|
window.WeatherChart = WeatherChart;
|
||||||
@ -1,112 +1,42 @@
|
|||||||
// 表格渲染功能
|
|
||||||
const WeatherTable = {
|
const WeatherTable = {
|
||||||
display(historyData = [], forecastData = []) {
|
display(historyData = [], forecastData = []) {
|
||||||
const tbody = document.getElementById('tableBody');
|
|
||||||
if (!tbody) return;
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
|
|
||||||
historyData = Array.isArray(historyData) ? historyData : [];
|
historyData = Array.isArray(historyData) ? historyData : [];
|
||||||
forecastData = Array.isArray(forecastData) ? forecastData : [];
|
forecastData = Array.isArray(forecastData) ? forecastData : [];
|
||||||
|
|
||||||
const nowTs = Date.now();
|
if (historyData.length === 0 && forecastData.length === 0) {
|
||||||
const future3hTs = nowTs + 3 * 60 * 60 * 1000;
|
return;
|
||||||
const showPastForecastEl = document.getElementById('showPastForecast');
|
}
|
||||||
const shouldShowPast = !!(showPastForecastEl && showPastForecastEl.checked);
|
|
||||||
|
|
||||||
const displayedForecast = forecastData.filter(item => {
|
const allData = [...historyData, ...forecastData];
|
||||||
const t = new Date(item.date_time).getTime();
|
const sortedData = allData.sort((a, b) => new Date(a.date_time) - new Date(b.date_time));
|
||||||
const isFuture3h = t > nowTs && t <= future3hTs;
|
|
||||||
const isPast = t <= nowTs;
|
const tableBody = document.getElementById('tableBody');
|
||||||
return isFuture3h || (shouldShowPast && isPast);
|
if (!tableBody) return;
|
||||||
|
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
const fmt = (v, digits) => (v === null || v === undefined || v === '' || isNaN(Number(v))) ? '' : Number(v).toFixed(digits);
|
||||||
|
|
||||||
|
sortedData.forEach(row => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${row.date_time || ''}</td>
|
||||||
|
<td>${fmt(row.temperature, 1)}</td>
|
||||||
|
<td>${fmt(row.humidity, 1)}</td>
|
||||||
|
<td>${fmt(row.pressure, 1)}</td>
|
||||||
|
<td>${fmt(row.wind_speed, 1)}</td>
|
||||||
|
<td>${fmt(row.wind_direction, 0)}</td>
|
||||||
|
<td>${fmt(row.rainfall, 2)}</td>
|
||||||
|
<td>${fmt(row.light, 0)}</td>
|
||||||
|
<td>${fmt(row.uv, 1)}</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
const hasForecast = displayedForecast.length > 0;
|
|
||||||
|
|
||||||
const forecastToggleContainer = document.getElementById('forecastToggleContainer');
|
const forecastToggleContainer = document.getElementById('forecastToggleContainer');
|
||||||
if (forecastToggleContainer) {
|
if (forecastToggleContainer) {
|
||||||
forecastToggleContainer.style.display = forecastData.length > 0 ? 'block' : 'none';
|
forecastToggleContainer.style.display = forecastData.length > 0 ? 'flex' : '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 th0 = document.createElement('th');
|
|
||||||
th0.textContent = '降水概率 (%)';
|
|
||||||
th0.className = 'bg-gray-50 font-semibold';
|
|
||||||
thead.appendChild(th0);
|
|
||||||
}
|
|
||||||
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 issuedBadge = (() => {
|
|
||||||
if (item.source !== '预报') return '';
|
|
||||||
const lead = (typeof item.lead_hours === 'number' && item.lead_hours >= 0) ? ` +${item.lead_hours}h` : '';
|
|
||||||
const issued = item.issued_at ? new Date(item.issued_at.replace(' ', 'T')) : null;
|
|
||||||
const issuedStr = issued ? `${String(issued.getHours()).padStart(2,'0')}:${String(issued.getMinutes()).padStart(2,'0')}` : '-';
|
|
||||||
return `<span style="font-size: 12px; color: #6b7280;">(发布: ${issuedStr}${lead})</span>`;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
`<td>${item.date_time}${hasForecast ? ` <span style="font-size: 12px; color: ${item.source === '预报' ? '#ff8c00' : '#28a745'};">[${item.source}]</span>` : ''}${item.source === '预报' ? `<br>${issuedBadge}` : ''}</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(
|
|
||||||
`${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? `<td>${item.precip_prob}</td>` : '<td>-</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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<script src="/static/js/chart.js"></script>
|
<script src="/static/js/chart.js"></script>
|
||||||
<!-- OpenLayers CSS and JS -->
|
|
||||||
<link rel="stylesheet" href="/static/css/ol.css">
|
<link rel="stylesheet" href="/static/css/ol.css">
|
||||||
<script src="/static/js/ol.js"></script>
|
<script src="/static/js/ol.js"></script>
|
||||||
<link rel="stylesheet" href="/static/css/tailwind.min.css">
|
<link rel="stylesheet" href="/static/css/tailwind.min.css">
|
||||||
@ -16,13 +15,11 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义:百分比左右内边距(避免 JIT 依赖)*/
|
|
||||||
.px-7p {
|
.px-7p {
|
||||||
padding-left: 7%;
|
padding-left: 7%;
|
||||||
padding-right: 7%;
|
padding-right: 7%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义:内容区宽度控制(避免 JIT 任意值类)*/
|
|
||||||
.content-narrow {
|
.content-narrow {
|
||||||
width: 86%;
|
width: 86%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@ -249,7 +246,6 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 设备列表样式 */
|
|
||||||
.device-modal {
|
.device-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -436,7 +432,6 @@
|
|||||||
<h1 class="text-2xl md:text-3xl font-semibold p-7">{{.Title}}</h1>
|
<h1 class="text-2xl md:text-3xl font-semibold p-7">{{.Title}}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设备列表 -->
|
|
||||||
<div id="deviceModal" class="device-modal" x-show="deviceModalOpen" x-transition.opacity @click.self="deviceModalOpen=false">
|
<div id="deviceModal" class="device-modal" x-show="deviceModalOpen" x-transition.opacity @click.self="deviceModalOpen=false">
|
||||||
<div class="device-modal-content bg-white shadow-xl" x-transition.scale.duration.150ms>
|
<div class="device-modal-content bg-white shadow-xl" x-transition.scale.duration.150ms>
|
||||||
<div class="device-list-header flex items-center justify-between border-b">
|
<div class="device-list-header flex items-center justify-between border-b">
|
||||||
@ -444,7 +439,6 @@
|
|||||||
<span class="close-modal" @click="deviceModalOpen=false">×</span>
|
<span class="close-modal" @click="deviceModalOpen=false">×</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="deviceList" class="device-list">
|
<div id="deviceList" class="device-list">
|
||||||
<!-- 设备列表将通过JavaScript动态填充 -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="device-list-footer">
|
<div class="device-list-footer">
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
@ -457,13 +451,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container content-narrow py-5">
|
<div class="container content-narrow py-5">
|
||||||
<!-- 系统信息 -->
|
|
||||||
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
|
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
|
||||||
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span> 个 |
|
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span> 个 |
|
||||||
<strong>总设备: </strong> <a href="#" id="showDeviceList" class="text-blue-600 hover:text-blue-700 underline-offset-2" @click.prevent="deviceModalOpen = true; window.WeatherApp.updateDeviceList(1)"><span id="wh65lpCount">0</span> 个</a>
|
<strong>总设备: </strong> <a href="#" id="showDeviceList" class="text-blue-600 hover:text-blue-700 underline-offset-2" @click.prevent="deviceModalOpen = true; window.WeatherApp.updateDeviceList(1)"><span id="wh65lpCount">0</span> 个</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 控制面板 -->
|
|
||||||
<div class="controls flex flex-col gap-4 mb-5 p-4 border rounded bg-white">
|
<div class="controls flex flex-col gap-4 mb-5 p-4 border rounded bg-white">
|
||||||
<div class="control-row flex items-center gap-4 flex-wrap">
|
<div class="control-row flex items-center gap-4 flex-wrap">
|
||||||
<div class="station-input-group flex items-center gap-1">
|
<div class="station-input-group flex items-center gap-1">
|
||||||
@ -499,34 +491,40 @@
|
|||||||
<select id="forecastProvider" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
<select id="forecastProvider" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
<option value="">不显示预报</option>
|
<option value="">不显示预报</option>
|
||||||
<option value="open-meteo">欧洲气象局</option>
|
<option value="open-meteo">欧洲气象局</option>
|
||||||
<option value="caiyun" selected>气象站</option>
|
<option value="caiyun" selected>中央气象台</option>
|
||||||
<option value="imdroid">英卓</option>
|
<option value="imdroid">英卓</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
|
<label for="legendMode" class="text-sm text-gray-600">图例展示:</label>
|
||||||
|
<select id="legendMode" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
|
<option value="combo_standard" selected>综合(标准)</option>
|
||||||
|
<option value="combo_compact">综合(精简)</option>
|
||||||
|
<option value="verify_all">预报验证(要素对比)</option>
|
||||||
|
<option value="temp_compare">温度对比</option>
|
||||||
|
<option value="hum_compare">湿度对比</option>
|
||||||
|
<option value="rain_all">降水(+1/+2/+3h)</option>
|
||||||
|
<option value="pressure_compare">气压对比</option>
|
||||||
|
<option value="wind_compare">风速对比</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group" id="timeRangeGroup">
|
||||||
<label for="startDate" class="text-sm text-gray-600">开始时间:</label>
|
<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">
|
<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>
|
<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">
|
<input type="datetime-local" id="endDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<button onclick="queryHistoryData()" id="queryBtn" class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-3 py-1 rounded">查看历史数据</button>
|
<button onclick="queryHistoryData()" id="queryBtn" class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-3 py-1 rounded">查看历史数据</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 地图容器 -->
|
|
||||||
<div class="map-container border border-gray-200 rounded" id="mapContainer">
|
<div class="map-container border border-gray-200 rounded" id="mapContainer">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
|
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图表容器 -->
|
|
||||||
<div class="chart-container" id="chartContainer">
|
<div class="chart-container" id="chartContainer">
|
||||||
<div id="stationInfoTitle" class="station-info-title"></div>
|
<div id="stationInfoTitle" class="station-info-title"></div>
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
@ -534,7 +532,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<div class="table-container" id="tableContainer">
|
<div class="table-container" id="tableContainer">
|
||||||
<div id="forecastToggleContainer" style="padding: 8px 12px; font-size: 12px; color: #666; display: none; display: flex; justify-content: center; align-items: center;">
|
<div id="forecastToggleContainer" style="padding: 8px 12px; font-size: 12px; color: #666; display: none; display: flex; justify-content: center; align-items: center;">
|
||||||
<label style="display: flex; align-items: center; gap: 5px;">
|
<label style="display: flex; align-items: center; gap: 5px;">
|
||||||
@ -557,7 +554,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="tableBody">
|
<tbody id="tableBody">
|
||||||
<!-- 数据行将动态填充 -->
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user