diff --git a/static/js/app.js b/static/js/app.js index 62ee7dd..6df910d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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); diff --git a/static/js/utils.js b/static/js/utils.js index 456294c..fc3aa78 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -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; \ No newline at end of file diff --git a/static/js/weather-chart.js b/static/js/weather-chart.js index 541c225..0a5cd95 100644 --- a/static/js/weather-chart.js +++ b/static/js/weather-chart.js @@ -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; \ No newline at end of file diff --git a/static/js/weather-table.js b/static/js/weather-table.js index 036d5d2..81e5330 100644 --- a/static/js/weather-table.js +++ b/static/js/weather-table.js @@ -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 = ` + ${row.date_time || ''} + ${fmt(row.temperature, 1)} + ${fmt(row.humidity, 1)} + ${fmt(row.pressure, 1)} + ${fmt(row.wind_speed, 1)} + ${fmt(row.wind_direction, 0)} + ${fmt(row.rainfall, 2)} + ${fmt(row.light, 0)} + ${fmt(row.uv, 1)} + `; + 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 `(发布: ${issuedStr}${lead})`; - })(); - - const columns = [ - `${item.date_time}${hasForecast ? ` [${item.source}]` : ''}${item.source === '预报' ? `
${issuedBadge}` : ''}`, - `${fmt2(item.temperature)}`, - `${fmt2(item.humidity)}`, - `${fmt2(item.pressure)}`, - `${fmt2(item.wind_speed)}`, - `${fmt2(item.wind_direction)}`, - `${fmt3(item.rainfall)}` - ]; - - if (hasForecast) { - columns.push( - `${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? `${item.precip_prob}` : '-'}` - ); - } - - columns.push( - `${(item.light !== null && item.light !== undefined) ? Number(item.light).toFixed(2) : '-'}`, - `${(item.uv !== null && item.uv !== undefined) ? Number(item.uv).toFixed(2) : '-'}` - ); - - row.innerHTML = columns.join(''); - tbody.appendChild(row); - }); } }; diff --git a/templates/index.html b/templates/index.html index 2722cdc..f4919d6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,6 @@ {{.Title}} - @@ -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 @@

{{.Title}}

-
@@ -444,7 +439,6 @@ ×
-