2025-11-03 16:06:37 +08:00

337 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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