const WeatherApp = { cachedHistoryData: [], cachedForecastData: [], currentPage: 1, itemsPerPage: 10, filteredDevices: [], init() { WeatherUtils.initializeDateInputs(); WeatherMap.init(window.TIANDITU_KEY || ''); WeatherMap.loadStations(); setInterval(() => this.updateOnlineDevices(), 30000); this.bindUI(); window.addEventListener('query-history-data', () => this.queryHistoryData()); // 大屏:每10分钟刷新一次雷达/雨量瓦片时次并更新显示 if (window.IS_BIGSCREEN) { // 防抖锁,避免重复请求 this._autoRefreshing = false; setInterval(async () => { try { // 刷新雷达/雨量瓦片时次 if (WeatherMap && typeof WeatherMap.reloadTileTimesAndShow === 'function') { const prodSel = document.getElementById('tileProduct'); const prod = prodSel ? prodSel.value : WeatherMap.tileProduct; if (prod && prod !== 'none') { WeatherMap.reloadTileTimesAndShow(); } } // 刷新图表/表格(基于当前控件选择) const sid = (document.getElementById('stationInput')?.value || '').trim(); if (!this._autoRefreshing && sid) { this._autoRefreshing = true; try { await this.queryHistoryData(); } finally { this._autoRefreshing = false; } } } catch (e) { console.warn('自动刷新失败', e); } }, 10 * 60 * 1000); } }, bindUI() { const stationInput = document.getElementById('stationInput'); if (stationInput) { stationInput.addEventListener('input', function() { this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, ''); }); stationInput.addEventListener('change', function() { const value = this.value.trim(); if (!value) return; if (/^[0-9A-F]+$/i.test(value)) { if (value.length <= 6) this.value = WeatherUtils.hexToDecimal(value); } else { const num = parseInt(value); if (!isNaN(num)) this.value = num.toString(); } }); } const showDeviceListBtn = document.getElementById('showDeviceList'); const modal = document.getElementById('deviceModal'); const closeBtn = document.querySelector('.close-modal'); const prevPageBtn = document.getElementById('prevPage'); const nextPageBtn = document.getElementById('nextPage'); const deviceListEl = document.getElementById('deviceList'); if (deviceListEl && modal) { deviceListEl.addEventListener('click', (e) => { const deviceItem = e.target.closest('.device-item'); if (!deviceItem) return; const decimalId = deviceItem.getAttribute('data-decimal-id'); const input = document.getElementById('stationInput'); if (input) input.value = decimalId; window.dispatchEvent(new CustomEvent('close-device-modal')); this.queryHistoryData(); }); } const showPastForecast = document.getElementById('showPastForecast'); if (showPastForecast) { showPastForecast.addEventListener('change', () => { WeatherTable.display(this.cachedHistoryData, this.cachedForecastData); }); } 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; deviceListContainer.innerHTML = ''; this.filteredDevices = (WeatherMap.stations || []) .filter(station => station.device_type === 'WH65LP') .sort((a, b) => { const aOnline = WeatherUtils.isDeviceOnline(a.last_update); const bOnline = WeatherUtils.isDeviceOnline(b.last_update); if (aOnline === bOnline) return 0; return aOnline ? -1 : 1; }); const totalPages = Math.ceil(this.filteredDevices.length / this.itemsPerPage) || 1; this.currentPage = Math.min(Math.max(1, page), totalPages); const currentPageEl = document.getElementById('currentPage'); const totalPagesEl = document.getElementById('totalPages'); const prevBtn = document.getElementById('prevPage'); const nextBtn = document.getElementById('nextPage'); if (currentPageEl) currentPageEl.textContent = this.currentPage; if (totalPagesEl) totalPagesEl.textContent = totalPages; if (prevBtn) prevBtn.disabled = this.currentPage <= 1; if (nextBtn) nextBtn.disabled = this.currentPage >= totalPages; const startIndex = (this.currentPage - 1) * this.itemsPerPage; const endIndex = startIndex + this.itemsPerPage; const currentDevices = this.filteredDevices.slice(startIndex, endIndex); currentDevices.forEach(device => { const isOnline = WeatherUtils.isDeviceOnline(device.last_update); const deviceItem = document.createElement('div'); deviceItem.className = 'device-item'; deviceItem.setAttribute('data-decimal-id', device.decimal_id); deviceItem.innerHTML = `
${device.decimal_id} | ${device.name} | ${device.location || '未知位置'}
${isOnline ? '在线' : '离线'} `; deviceListContainer.appendChild(deviceItem); }); if (this.filteredDevices.length === 0) { deviceListContainer.innerHTML = '
暂无WH65LP设备
'; } }, async updateOnlineDevices() { try { const response = await fetch('/api/system/status'); const data = await response.json(); const onlineEl = document.getElementById('onlineDevices'); if (onlineEl) onlineEl.textContent = data.online_devices; } catch (error) { console.error('更新在线设备数量失败:', error); } }, async queryHistoryData() { const decimalId = (document.getElementById('stationInput')?.value || '').trim(); if (!decimalId) { alert('请输入站点编号'); return; } if (!/^\d+$/.test(decimalId)) { alert('请输入有效的十进制编号'); return; } const startTime = document.getElementById('startDate')?.value || ''; const endTime = document.getElementById('endDate')?.value || ''; const interval = document.getElementById('interval')?.value || '1hour'; const forecastProvider = document.getElementById('forecastProvider')?.value || ''; if (!startTime || !endTime) { alert('请选择开始和结束时间'); return; } try { const historyParams = new URLSearchParams({ decimal_id: decimalId, start_time: startTime.replace('T', ' ') + ':00', end_time: endTime.replace('T', ' ') + ':00', interval: interval }); const historyResponse = await fetch(`/api/data?${historyParams}`); if (!historyResponse.ok) throw new Error('查询历史数据失败'); const responseData = await historyResponse.json(); const historyData = Array.isArray(responseData) ? responseData : []; let forecastData = []; if (forecastProvider && interval === '1hour') { try { const hexID = WeatherUtils.decimalToHex(decimalId); const stationID = `RS485-${hexID}`; // 将预报查询范围按小时对齐:from 向下取整到整点;to 向上取整到整点,再 +3h const parseLocal = (s) => { try { return new Date(s); } catch { return null; } }; const floorHour = (d) => { const t = new Date(d); t.setMinutes(0,0,0); return t; }; const ceilHour = (d) => { const t = new Date(d); if (t.getMinutes()||t.getSeconds()||t.getMilliseconds()) { t.setHours(t.getHours()+1); } t.setMinutes(0,0,0); return t; }; const fmt = (d) => { const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const da=String(d.getDate()).padStart(2,'0'); const h=String(d.getHours()).padStart(2,'0'); const mi='00'; const s='00'; return `${y}-${m}-${da} ${h}:${mi}:${s}`; }; const startD = parseLocal(startTime); const endD = parseLocal(endTime); const fromStr = startD && !isNaN(startD) ? fmt(floorHour(startD)) : startTime.replace('T',' ') + ':00'; const toStr = endD && !isNaN(endD) ? fmt(new Date(ceilHour(endD).getTime() + 3*60*60*1000)) : endTime.replace('T',' ') + ':00'; const forecastParams = new URLSearchParams({ station_id: stationID, from: fromStr, to: toStr, provider: forecastProvider, versions: '3' }); const forecastResponse = await fetch(`/api/forecast?${forecastParams}`); if (forecastResponse.ok) { const responseData = await forecastResponse.json(); forecastData = Array.isArray(responseData) ? responseData : []; console.log(`查询到 ${forecastData.length} 条预报数据`); } } catch (e) { console.warn('查询预报数据失败:', e); } } this.cachedHistoryData = historyData; this.cachedForecastData = forecastData; if (historyData.length === 0 && forecastData.length === 0) { alert('该时间段内无数据'); return; } const station = (WeatherMap.stations || []).find(s => s.decimal_id == decimalId); const stationInfoTitle = document.getElementById('stationInfoTitle'); if (stationInfoTitle) { // 仅展示站点位置(location);无有效值时保持现有文本不变 const loc = station?.location || ''; if (loc) stationInfoTitle.textContent = loc; } if (!WeatherMap.isMapCollapsed) WeatherMap.toggleMap(); WeatherChart.display(historyData, forecastData); WeatherTable.display(historyData, forecastData); const chartContainer = document.getElementById('chartContainer'); const tableContainer = document.getElementById('tableContainer'); if (chartContainer) chartContainer.classList.add('show'); if (tableContainer) tableContainer.classList.add('show'); setTimeout(() => { chartContainer?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 300); const legendMode = document.getElementById('legendMode'); if (legendMode) { WeatherChart.applyLegendMode(legendMode.value); } // 更新大屏摘要区域(未来降雨与过去准确率) this.updateSummaryPanel(historyData, forecastData); } catch (error) { console.error('查询数据失败:', error); alert('查询数据失败: ' + error.message); } } }; window.WeatherApp = WeatherApp; document.addEventListener('DOMContentLoaded', () => { WeatherApp.init(); }); // 扩展:用于大屏摘要展示 WeatherApp.updateSummaryPanel = function(historyData, forecastData){ try{ const elFuture = document.getElementById('futureRainSummary'); const elAcc = document.getElementById('pastAccuracySummary'); if (!elFuture && !elAcc) return; // --- 未来1~3小时降雨 --- const fmt = (n)=> (n==null||isNaN(Number(n))) ? '--' : Number(n).toFixed(1); const pad2 = (n)=> String(n).padStart(2,'0'); const fmtDT = (d)=> `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:00:00`; const now = new Date(); const ceilHour = (d)=>{ const t = new Date(d); if (t.getMinutes()||t.getSeconds()||t.getMilliseconds()) t.setHours(t.getHours()+1); t.setMinutes(0,0,0); return t; }; const t1 = ceilHour(now), t2 = new Date(t1.getTime()+3600*1000), t3 = new Date(t1.getTime()+2*3600*1000); const pickBestAt = (dtStr)=>{ const cand = (forecastData||[]).filter(x=>x.date_time===dtStr && x.rainfall!=null); if (cand.length===0) return null; // 选择最小 lead_hours 的记录 cand.sort((a,b)=> (a.lead_hours??99)-(b.lead_hours??99)); return cand[0].rainfall; }; const r1 = pickBestAt(fmtDT(t1)); const r2 = pickBestAt(fmtDT(t2)); const r3 = pickBestAt(fmtDT(t3)); const futureSum = [r1,r2,r3].reduce((s,v)=> s + (v!=null?Number(v):0), 0); if (elFuture) elFuture.textContent = `未来1~3小时降雨 ${fmt(futureSum)} 毫米`; // 预警等级:>0.4 红色;>0.0 橙色;否则无 try { const panel = document.getElementById('summaryPanel'); const screen = document.querySelector('.screen'); const setLevel = (sum) => { const isRed = sum > 1; const isOrange = !isRed && sum > 0 && sum < 1; if (panel) { panel.classList.toggle('alert-red', isRed); panel.classList.toggle('alert-on', isOrange); if (!isRed && !isOrange) { panel.classList.remove('alert-red','alert-on'); } } if (screen) { screen.classList.toggle('alert-red', isRed); screen.classList.toggle('alert-on', isOrange); if (!isRed && !isOrange) { screen.classList.remove('alert-red','alert-on'); } } }; setLevel(futureSum); } catch {} // --- 过去预报准确率(按小时分档 [0,5), [5,10), [10,∞))--- const bucketOf = (mm)=>{ if (mm==null || isNaN(Number(mm))) return null; const v = Math.max(0, Number(mm)); if (v < 5) return 0; if (v < 10) return 1; return 2; }; const rainActual = new Map(); (historyData||[]).forEach(it=>{ if (it && it.date_time) rainActual.set(it.date_time, it.rainfall); }); const tally = (lead)=>{ let correct=0,total=0; (forecastData||[]).forEach(f=>{ if (f.lead_hours!==lead) return; const a = rainActual.get(f.date_time); if (a==null) return; const ba=bucketOf(a), bf=bucketOf(f.rainfall); if (ba==null || bf==null) return; total += 1; if (ba===bf) correct += 1; }); return {correct,total}; }; const rH1 = tally(1), rH2 = tally(2), rH3 = tally(3); const pct = (r)=> r.total>0 ? `${(r.correct/r.total*100).toFixed(1)}%` : '--'; if (elAcc) elAcc.textContent = `过去预报准确率 +1h: ${pct(rH1)} +2h: ${pct(rH2)} +3h: ${pct(rH3)}`; }catch(e){ console.warn('更新摘要失败', e); } }