337 lines
16 KiB
JavaScript
337 lines
16 KiB
JavaScript
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 = `
|
||
<div style="font-size: 13px; color: #444">
|
||
${device.decimal_id} | ${device.name} | ${device.location || '未知位置'}
|
||
</div>
|
||
<span style="color: ${isOnline ? '#28a745' : '#dc3545'}; font-size: 12px; padding: 2px 6px; background: ${isOnline ? '#f0f9f1' : '#fef5f5'}; border-radius: 3px">${isOnline ? '在线' : '离线'}</span>
|
||
`;
|
||
deviceListContainer.appendChild(deviceItem);
|
||
});
|
||
|
||
if (this.filteredDevices.length === 0) {
|
||
deviceListContainer.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">暂无WH65LP设备</div>';
|
||
}
|
||
},
|
||
|
||
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.4mm)触发 Summary 面板橙色预警样式
|
||
try {
|
||
const panel = document.getElementById('summaryPanel');
|
||
if (panel) {
|
||
if (futureSum > 0.4) panel.classList.add('alert-on');
|
||
else panel.classList.remove('alert-on');
|
||
}
|
||
} 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); }
|
||
}
|