feat: 优化前端页面

This commit is contained in:
yarnom 2025-09-01 18:16:07 +08:00
parent d26c2c025a
commit 9cd26d3df3
5 changed files with 176 additions and 214 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);
});
} }
}; };

View File

@ -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>