feat: 优化前端页面
This commit is contained in:
parent
d26c2c025a
commit
9cd26d3df3
@ -1,4 +1,3 @@
|
||||
// 应用主控制器
|
||||
const WeatherApp = {
|
||||
cachedHistoryData: [],
|
||||
cachedForecastData: [],
|
||||
@ -7,16 +6,11 @@ const WeatherApp = {
|
||||
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());
|
||||
},
|
||||
|
||||
@ -44,30 +38,6 @@ const WeatherApp = {
|
||||
const prevPageBtn = document.getElementById('prevPage');
|
||||
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');
|
||||
if (deviceListEl && modal) {
|
||||
deviceListEl.addEventListener('click', (e) => {
|
||||
@ -76,7 +46,6 @@ const WeatherApp = {
|
||||
const decimalId = deviceItem.getAttribute('data-decimal-id');
|
||||
const input = document.getElementById('stationInput');
|
||||
if (input) input.value = decimalId;
|
||||
// 关闭交给 Alpine: deviceModalOpen = false
|
||||
window.dispatchEvent(new CustomEvent('close-device-modal'));
|
||||
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.toggleMap = () => WeatherMap.toggleMap();
|
||||
window.queryHistoryData = () => this.queryHistoryData();
|
||||
},
|
||||
|
||||
// 更新设备列表
|
||||
updateDeviceList(page = 1) {
|
||||
const deviceListContainer = document.getElementById('deviceList');
|
||||
if (!deviceListContainer) return;
|
||||
@ -247,6 +224,11 @@ const WeatherApp = {
|
||||
setTimeout(() => {
|
||||
chartContainer?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 300);
|
||||
|
||||
const legendMode = document.getElementById('legendMode');
|
||||
if (legendMode) {
|
||||
WeatherChart.applyLegendMode(legendMode.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询数据失败:', error);
|
||||
alert('查询数据失败: ' + error.message);
|
||||
|
||||
@ -1,49 +1,39 @@
|
||||
// 工具函数
|
||||
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小时
|
||||
const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const endDate = new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
||||
|
||||
document.getElementById('startDate').value = this.formatDatetimeLocal(startDate);
|
||||
document.getElementById('endDate').value = this.formatDatetimeLocal(endDate);
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
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;
|
||||
@ -1,25 +1,19 @@
|
||||
// 图表相关功能
|
||||
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;
|
||||
}
|
||||
|
||||
// 合并历史数据和预报数据的时间轴(按 date_time 对齐)
|
||||
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;
|
||||
@ -45,7 +39,6 @@ const WeatherChart = {
|
||||
return item && item.rain_total !== undefined ? item.rain_total : null;
|
||||
});
|
||||
|
||||
// 预报:按同一 forecast_time 的不同 lead_hours 组织(仅用 0..3h)
|
||||
const byTime = new Map();
|
||||
forecastData.forEach(fp => {
|
||||
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 pickNearest = (label, field) => {
|
||||
// 近似优先级:0h > 1h > 2h > 3h
|
||||
const b = byTime.get(label);
|
||||
if (!b) return null;
|
||||
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 forecastWindSpeedsNearest = allLabels.map(label => pickNearest(label, 'wind_speed'));
|
||||
|
||||
// 销毁旧图表
|
||||
if (this.chart) this.chart.destroy();
|
||||
|
||||
// 数据集
|
||||
const datasets = [
|
||||
{
|
||||
label: '温度 (°C) - 实测',
|
||||
seriesKey: 'temp_actual',
|
||||
data: historyTemperatures,
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
@ -103,6 +94,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '湿度 (%) - 实测',
|
||||
seriesKey: 'hum_actual',
|
||||
data: historyHumidities,
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
@ -113,6 +105,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '大气压 (hPa) - 实测',
|
||||
seriesKey: 'pressure_actual',
|
||||
data: historyPressures,
|
||||
borderColor: 'rgb(153, 102, 255)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||
@ -123,6 +116,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '风速 (m/s) - 实测',
|
||||
seriesKey: 'wind_actual',
|
||||
data: historyWindSpeeds,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
@ -133,6 +127,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '雨量 (mm) - 实测',
|
||||
seriesKey: 'rain_actual',
|
||||
data: historyRainfalls,
|
||||
type: 'bar',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||
@ -141,6 +136,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '累计雨量 (mm) - 实测',
|
||||
seriesKey: 'rain_total',
|
||||
data: historyRainTotals,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
@ -153,27 +149,28 @@ const WeatherChart = {
|
||||
];
|
||||
|
||||
if (forecastData.length > 0) {
|
||||
// 雨量 仅显示 -1h/-2h/-3h
|
||||
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) - 预报 (+2h)', 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) - 预报 (+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)', 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)', 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(
|
||||
{
|
||||
label: '温度 (°C) - 预报',
|
||||
seriesKey: 'temp_fcst',
|
||||
data: forecastTemperaturesNearest,
|
||||
borderColor: 'rgb(255, 159, 64)',
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||
borderDash: [5, 5],
|
||||
yAxisID: 'y-temperature',
|
||||
tension: 0.4,
|
||||
spanGaps: false
|
||||
spanGaps: false,
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
label: '湿度 (%) - 预报',
|
||||
seriesKey: 'hum_fcst',
|
||||
data: forecastHumiditiesNearest,
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
@ -185,6 +182,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '大气压 (hPa) - 预报',
|
||||
seriesKey: 'pressure_fcst',
|
||||
data: forecastPressuresNearest,
|
||||
borderColor: 'rgb(153, 102, 255)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.1)',
|
||||
@ -196,6 +194,7 @@ const WeatherChart = {
|
||||
},
|
||||
{
|
||||
label: '风速 (m/s) - 预报',
|
||||
seriesKey: 'wind_fcst',
|
||||
data: forecastWindSpeedsNearest,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
@ -208,9 +207,8 @@ const WeatherChart = {
|
||||
);
|
||||
}
|
||||
|
||||
// 创建组合图表
|
||||
const ctx = document.getElementById('combinedChart').getContext('2d');
|
||||
this.chart = new Chart(ctx, {
|
||||
const chartConfig = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: allLabels,
|
||||
@ -235,7 +233,7 @@ const WeatherChart = {
|
||||
'y-temperature': {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
position: 'right',
|
||||
title: { display: true, text: '温度 (°C)' }
|
||||
},
|
||||
'y-humidity': {
|
||||
@ -250,14 +248,14 @@ const WeatherChart = {
|
||||
'y-pressure': {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
position: 'left',
|
||||
title: { display: true, text: '大气压 (hPa)' },
|
||||
grid: { drawOnChartArea: false }
|
||||
},
|
||||
'y-wind': {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
position: 'left',
|
||||
title: { display: true, text: '风速 (m/s)' },
|
||||
grid: { drawOnChartArea: false },
|
||||
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;
|
||||
@ -1,112 +1,42 @@
|
||||
// 表格渲染功能
|
||||
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 : [];
|
||||
|
||||
if (historyData.length === 0 && forecastData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nowTs = Date.now();
|
||||
const future3hTs = nowTs + 3 * 60 * 60 * 1000;
|
||||
const showPastForecastEl = document.getElementById('showPastForecast');
|
||||
const shouldShowPast = !!(showPastForecastEl && showPastForecastEl.checked);
|
||||
const allData = [...historyData, ...forecastData];
|
||||
const sortedData = allData.sort((a, b) => new Date(a.date_time) - new Date(b.date_time));
|
||||
|
||||
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 tableBody = document.getElementById('tableBody');
|
||||
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');
|
||||
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">
|
||||
<title>{{.Title}}</title>
|
||||
<script src="/static/js/chart.js"></script>
|
||||
<!-- OpenLayers CSS and JS -->
|
||||
<link rel="stylesheet" href="/static/css/ol.css">
|
||||
<script src="/static/js/ol.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/tailwind.min.css">
|
||||
@ -16,13 +15,11 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 自定义:百分比左右内边距(避免 JIT 依赖)*/
|
||||
.px-7p {
|
||||
padding-left: 7%;
|
||||
padding-right: 7%;
|
||||
}
|
||||
|
||||
/* 自定义:内容区宽度控制(避免 JIT 任意值类)*/
|
||||
.content-narrow {
|
||||
width: 86%;
|
||||
max-width: 1200px;
|
||||
@ -249,7 +246,6 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 设备列表样式 */
|
||||
.device-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -436,7 +432,6 @@
|
||||
<h1 class="text-2xl md:text-3xl font-semibold p-7">{{.Title}}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<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-list-header flex items-center justify-between border-b">
|
||||
@ -444,7 +439,6 @@
|
||||
<span class="close-modal" @click="deviceModalOpen=false">×</span>
|
||||
</div>
|
||||
<div id="deviceList" class="device-list">
|
||||
<!-- 设备列表将通过JavaScript动态填充 -->
|
||||
</div>
|
||||
<div class="device-list-footer">
|
||||
<div class="pagination">
|
||||
@ -457,13 +451,11 @@
|
||||
</div>
|
||||
|
||||
<div class="container content-narrow py-5">
|
||||
<!-- 系统信息 -->
|
||||
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<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="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">
|
||||
<option value="">不显示预报</option>
|
||||
<option value="open-meteo">欧洲气象局</option>
|
||||
<option value="caiyun" selected>气象站</option>
|
||||
<option value="caiyun" selected>中央气象台</option>
|
||||
<option value="imdroid">英卓</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input type="datetime-local" id="startDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="endDate" class="text-sm text-gray-600">结束时间:</label>
|
||||
<input type="datetime-local" id="endDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button 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 class="map-container border border-gray-200 rounded" id="mapContainer">
|
||||
<div id="map"></div>
|
||||
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
|
||||
</div>
|
||||
|
||||
<!-- 图表容器 -->
|
||||
<div class="chart-container" id="chartContainer">
|
||||
<div id="stationInfoTitle" class="station-info-title"></div>
|
||||
<div class="chart-wrapper">
|
||||
@ -534,7 +532,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<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;">
|
||||
<label style="display: flex; align-items: center; gap: 5px;">
|
||||
@ -557,7 +554,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<!-- 数据行将动态填充 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user