yarnom cfb0bca723 Revert "feat: 新增雷达图"
This reverts commit df7358530f428751cdbce3f4220f1925e7b616c2.
2025-09-23 09:33:00 +08:00

963 lines
39 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<script src="/static/js/chart.js"></script>
<link rel="stylesheet" href="/static/css/ol.css">
<script src="/static/js/ol.js"></script>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<link rel="stylesheet" href="/static/css/tailwind.min.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.px-7p {
padding-left: 7%;
padding-right: 7%;
}
.content-narrow {
width: 86%;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 768px) {
.content-narrow {
width: 92%;
}
}
.header {
padding: 10px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 15px;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.control-row {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
flex-shrink: 0;
}
.station-input-group {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
#stationInput {
width: 120px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
select, input, button {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.map-container {
height: 60vh;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
position: relative;
transition: height 0.3s ease;
overflow: hidden;
}
.map-container.collapsed {
height: 38vh;
}
#map {
width: 100%;
height: 100%;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 5px;
}
.map-control-btn {
padding: 3px 8px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
color: #666;
}
.map-control-btn:hover {
background-color: white;
}
.map-control-btn.active {
background-color: #007bff;
color: white;
}
.map-toggle-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 1001;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
color: white;
background-color: #007bff;
}
.station-info-title {
text-align: center;
margin-bottom: 15px;
padding: 10px;
font-size: 14px;
line-height: 1.6;
}
.chart-container {
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
background-color: #fff;
display: none;
}
.chart-container.show {
display: block;
animation: slideDown 0.3s ease;
}
.chart-wrapper {
height: 500px;
margin-bottom: 30px;
}
.table-container {
overflow-x: auto;
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
display: none;
}
.table-container.show {
display: block;
animation: slideDown 0.3s ease;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: center;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f5f5f5;
}
/* Radar view image normalization */
.radar-grid .img-wrap {
position: relative;
width: 75%;
margin: 0 auto;
aspect-ratio: 4/3;
background: #fafafa;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.radar-grid .img-wrap { min-height: 220px; }
@media (min-width: 768px) { .radar-grid .img-wrap { min-height: 320px; } }
@media (min-width: 1024px) { .radar-grid .img-wrap { min-height: 360px; } }
.radar-grid .img-wrap img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
.system-info {
background-color: #e9ecef;
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 14px;
}
.device-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
}
.device-modal-content {
position: relative;
background-color: #fff;
height: auto;
max-height: 70vh;
width: 90%;
max-width: 720px;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
}
.device-list-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
position: relative;
flex-shrink: 0;
}
.device-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
position: relative;
}
.device-list-footer {
padding: 10px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
}
.pagination {
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
color: #666;
}
.pagination-btn {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 3px;
background: #fff;
cursor: pointer;
font-size: 12px;
color: #666;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.device-item {
padding: 12px 20px;
border-bottom: 1px solid #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.device-item:hover {
background-color: #fafafa;
}
.device-item:last-child {
border-bottom: none;
}
.close-modal {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #f5f5f5;
cursor: pointer;
color: #666;
font-size: 18px;
line-height: 1;
}
.close-modal:hover {
background-color: #eee;
color: #333;
}
.error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
}
.station-marker {
width: 24px;
height: 24px;
background-color: #007bff;
border: 2px solid white;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.station-marker.offline {
background-color: #6c757d;
}
.station-label {
background-color: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #007bff;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.control-group {
width: 100%;
}
select, input {
width: 100%;
}
.map-container {
height: 50vh;
}
}
</style>
</head>
<body x-data="{ showPastForecast: false, deviceModalOpen: false }" class="text-[14px] md:text-[15px]" x-init="window.addEventListener('close-device-modal', () => { deviceModalOpen = false })">
<div class="header p-2 text-center border-b border-gray-200">
<h1 class="text-2xl md:text-3xl font-semibold p-7">{{.Title}}</h1>
</div>
<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-list-header flex items-center justify-between border-b">
<div class="text-sm">设备列表</div>
<span class="close-modal" @click="deviceModalOpen=false">×</span>
</div>
<div id="deviceList" class="device-list">
</div>
<div class="device-list-footer">
<div class="pagination">
<button class="pagination-btn" id="prevPage" :disabled="window.WeatherApp.currentPage <= 1" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage - 1)">&lt; 上一页</button>
<span><span id="currentPage">1</span> 页,共 <span id="totalPages">1</span></span>
<button class="pagination-btn" id="nextPage" :disabled="window.WeatherApp.currentPage >= Math.ceil(window.WeatherApp.filteredDevices.length / window.WeatherApp.itemsPerPage)" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage + 1)">下一页 &gt;</button>
</div>
</div>
</div>
</div>
<div class="container content-narrow py-5">
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
<div class="flex items-center justify-between gap-3">
<div>
<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>
</div>
<nav class="flex items-center gap-2">
<a id="tab-station" href="#station" class="px-3 py-1 rounded text-sm font-medium bg-blue-600 text-white">气象站</a>
<a id="tab-radar" href="#radar" class="px-3 py-1 rounded text-sm text-blue-700 hover:bg-blue-50">南宁雷达</a>
</nav>
</div>
</div>
<div id="view-station">
<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="station-input-group flex items-center gap-1">
<label for="stationInput" class="text-sm text-gray-600">站点编号:</label>
<input type="text" id="stationInput" placeholder="" class="w-32 px-2 py-1 border border-gray-300 rounded text-sm font-mono">
</div>
<div class="control-group">
<label for="mapType" class="text-sm text-gray-600">地图类型:</label>
<select id="mapType" onchange="switchLayer(this.value)" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="satellite">卫星图</option>
<option value="vector">矢量图</option>
<option value="terrain">地形图</option>
<option value="hybrid">混合地形图</option>
</select>
</div>
<div class="control-group">
<label for="forecastProvider" class="text-sm text-gray-600">预报源:</label>
<select id="forecastProvider" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="">不显示预报</option>
<option value="open-meteo">欧洲气象局</option>
<option value="caiyun" selected>彩云</option>
<option value="imdroid">英卓</option>
<option value="cma">中央气象台</option>
</select>
</div>
<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="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>
<div class="control-row flex items-center gap-4 flex-wrap">
<div class="control-group flex items-center gap-1">
<label for="interval" class="text-sm text-gray-600">数据粒度:</label>
<select id="interval" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="raw">原始(16s)</option>
<option value="10min">10分钟</option>
<option value="30min">30分钟</option>
<option value="1hour" selected>1小时</option>
</select>
</div>
<div class="control-group" id="timeRangeGroup">
<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">
<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">
<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 class="map-container border border-gray-200 rounded" id="mapContainer">
<div id="map"></div>
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
</div>
<div class="chart-container" id="chartContainer">
<div id="stationInfoTitle" class="station-info-title"></div>
<div class="chart-wrapper">
<canvas id="combinedChart"></canvas>
</div>
</div>
<div class="table-container" id="tableContainer">
<div id="forecastToggleContainer" style="padding: 8px 12px;font-size: 12px;color: #666;display: none;display: flex;justify-content: flex-start;align-items: center;align-content: center;">
<label style="display: flex;align-items: center;gap: 5px;">
<input type="checkbox" id="showPastForecast" style="vertical-align: middle;" x-model="showPastForecast" @change="window.WeatherTable.display(window.WeatherApp.cachedHistoryData, window.WeatherApp.cachedForecastData)">
显示历史预报
</label>
</div>
<table class="min-w-full text-sm text-center">
<thead>
<tr id="tableHeader">
<th>时间</th>
<th>温度 (°C)</th>
<th>湿度 (%)</th>
<th>气压 (hPa)</th>
<th>风速 (m/s)</th>
<th>风向 (°)</th>
<th>雨量 (mm)</th>
<th>光照 (lux)</th>
<th>紫外线</th>
</tr>
</thead>
<tbody id="tableBody">
</tbody>
</table>
</div>
</div>
<div id="view-radar" style="display: none;">
<div class="bg-white border border-gray-200 rounded p-4 text-gray-700 radar-grid">
<div id="radarInfo" class="text-sm mb-3">正在加载最新雷达数据...</div>
<div class="flex items-center gap-2 mb-3 text-sm">
<button id="radar-tab-china" class="px-3 py-1 rounded bg-blue-600 text-white">中国</button>
<button id="radar-tab-huanan" class="px-3 py-1 rounded bg-gray-100 text-blue-700 hover:bg-blue-50">华南</button>
<button id="radar-tab-nanning" class="px-3 py-1 rounded bg-gray-100 text-blue-700 hover:bg-blue-50">南宁</button>
<button id="radar-tab-cma" class="px-3 py-1 rounded bg-gray-100 text-blue-700 hover:bg-blue-50">CMA</button>
</div>
<div class="img-wrap">
<img id="radar-main-img" alt="radar" />
</div>
<div id="radar-heat-section" class="mt-4">
<div class="text-xs text-gray-500 mb-1">二维渲染dBZ</div>
<div class="w-full flex justify-center">
<div id="radar-heat-plot" style="width:75%;max-width:640px;"></div>
</div>
</div>
</div>
</div>
</div>
<script>
window.TIANDITU_KEY = '{{.TiandituKey}}';
</script>
<script>
(function() {
function getViewFromHash() {
return (location.hash || '#station').replace('#','');
}
function setActive(view) {
var stationView = document.getElementById('view-station');
var radarView = document.getElementById('view-radar');
var tabStation = document.getElementById('tab-station');
var tabRadar = document.getElementById('tab-radar');
var activeClasses = ['bg-blue-600', 'text-white', 'font-medium'];
var inactiveClasses = ['text-blue-700', 'hover:bg-blue-50'];
if (view === 'radar') {
stationView.style.display = 'none';
radarView.style.display = 'block';
tabStation.classList.remove.apply(tabStation.classList, activeClasses);
inactiveClasses.forEach(c=>tabStation.classList.add(c));
inactiveClasses.forEach(c=>tabRadar.classList.remove(c));
activeClasses.forEach(c=>tabRadar.classList.add(c));
} else {
stationView.style.display = 'block';
radarView.style.display = 'none';
tabRadar.classList.remove.apply(tabRadar.classList, activeClasses);
inactiveClasses.forEach(c=>tabRadar.classList.add(c));
inactiveClasses.forEach(c=>tabStation.classList.remove(c));
activeClasses.forEach(c=>tabStation.classList.add(c));
}
}
function initTabs() {
var view = getViewFromHash();
setActive(view);
if (view === 'radar') { loadRadarLatest(); loadPlotGrid(); }
}
window.addEventListener('hashchange', initTabs);
document.addEventListener('DOMContentLoaded', function() {
var ts = document.getElementById('tab-station');
var tr = document.getElementById('tab-radar');
if (ts) ts.addEventListener('click', function(e){ e.preventDefault(); location.hash = '#station';});
if (tr) tr.addEventListener('click', function(e){ e.preventDefault(); location.hash = '#radar';});
initTabs();
});
async function loadRadarLatest() {
var infoEl = document.getElementById('radarInfo');
try {
const res = await fetch('/api/radar/latest');
if (!res.ok) throw new Error('no data');
const data = await res.json();
const meta = data.meta || {};
const images = data.images || {};
infoEl.textContent = '时间: ' + (meta.timestamp_local || '未知');
window.RadarLatestImages = images;
setRadarImage('china');
bindRadarTabs();
} catch (e) {
infoEl.textContent = '暂无最新雷达数据';
}
}
function bindRadarTabs() {
var ids = ['china','huanan','nanning','cma'];
ids.forEach(function(k){
var el = document.getElementById('radar-tab-' + k);
if (el) el.onclick = function(){ setRadarImage(k); };
});
}
function setRadarImage(kind) {
var images = window.RadarLatestImages || {};
var url = images[kind];
var img = document.getElementById('radar-main-img');
if (url) { img.src = url + '?t=' + Date.now(); }
// toggle active styles
var ids = ['china','huanan','nanning','cma'];
ids.forEach(function(k){
var el = document.getElementById('radar-tab-' + k);
if (!el) return;
if (k === kind) {
el.classList.add('bg-blue-600','text-white');
el.classList.remove('bg-gray-100','text-blue-700');
} else {
el.classList.remove('bg-blue-600','text-white');
el.classList.add('bg-gray-100','text-blue-700');
}
});
}
async function loadPlotGrid(){
const res = await fetch('/api/radar/latest/grid');
if (!res.ok) return;
const data = await res.json();
window.RadarLatestGrid = data;
renderPlotlyHeat(data);
renderClustersPanel();
renderWindQueryList();
renderWindResults();
}
function renderPlotlyHeat(payload){
// Preserve real dBZ values for z (so hover/scale show dBZ)
var z = (payload.grid || []).slice();
var colors = [
'#0000F6','#01A0F6','#00ECEC','#01FF00','#00C800',
'#019000','#FFFF00','#E7C000','#FF9000','#FF0000',
'#D60000','#C00000','#FF00F0','#780084','#AD90F0'
];
// Build step-like colorscale over dBZ domain [0, 5*(n-1)]
var zmin = 0;
var zmax = 5 * (colors.length - 1); // 70 for 15 colors
var colorscale = [];
for (var i=0;i<colors.length;i++){
var lo = (i*5)/zmax;
var hi = ((i+1)*5)/zmax;
if (i === colors.length-1) hi = 1.0;
// duplicate stops to create discrete bands
colorscale.push([lo, colors[i]]);
colorscale.push([hi, colors[i]]);
}
// Compute lon/lat arrays from bounds + res for hover/axes
var w = (payload.bounds && typeof payload.bounds.west === 'number') ? payload.bounds.west : 0;
var s = (payload.bounds && typeof payload.bounds.south === 'number') ? payload.bounds.south : 0;
var e = (payload.bounds && typeof payload.bounds.east === 'number') ? payload.bounds.east : 256;
var n = (payload.bounds && typeof payload.bounds.north === 'number') ? payload.bounds.north : 256;
var res = (typeof payload.res_deg === 'number' && payload.res_deg > 0) ? payload.res_deg : ((e - w) / 256.0);
// Use pixel centers for coordinates
var xs = new Array(256);
var ys = new Array(256);
for (var xi = 0; xi < 256; xi++) { xs[xi] = w + (xi + 0.5) * res; }
for (var yi = 0; yi < 256; yi++) { ys[yi] = s + (yi + 0.5) * res; }
// Build customdata to carry pixel (x,y) indices for hover
var cd = new Array(256);
for (var r = 0; r < 256; r++) {
var row = new Array(256);
for (var c = 0; c < 256; c++) {
row[c] = [c, r];
}
cd[r] = row;
}
var trace = {
z: z,
x: xs,
y: ys,
type: 'heatmap',
colorscale: colorscale,
colorbar: { title: 'dBZ', thickness: 18 },
zauto: false,
zmin: zmin,
zmax: zmax,
zsmooth: false,
customdata: cd,
hovertemplate: 'x=%{customdata[0]}, y=%{customdata[1]}<br>lon=%{x:.6f}, lat=%{y:.6f}<br>dBZ=%{z:.1f}<extra></extra>'
};
var box = document.getElementById('radar-heat-plot');
var size = Math.max(220, Math.min(520, Math.floor((box.clientWidth || 520))));
var cbx = 60; // approximate space for colorbar + padding
var layout = {
margin: {l:50, r:10, t:10, b:40},
xaxis: {
tickformat: '.3f',
range: [w, e]
},
yaxis: {
tickformat: '.3f',
range: [s, n]
},
width: size + cbx,
height: size,
};
var config = {displayModeBar:false, responsive:true};
Plotly.newPlot('radar-heat-plot', [trace], layout, config).then(function(){
window.addEventListener('resize', function(){
var s2 = Math.max(220, Math.min(520, Math.floor((box.clientWidth || 520))));
Plotly.relayout('radar-heat-plot', {width: s2 + cbx, height: s2});
});
});
}
function renderClustersPanel(){
// fetch meta to read clusters
fetch('/api/radar/latest').then(r=>r.json()).then(function(resp){
var meta = resp.meta || {};
var clusters = meta.clusters || [];
var host = '/radar/latest/';
var containerId = 'radar-clusters';
var parent = document.getElementById(containerId);
if (!parent) {
var sec = document.createElement('div');
sec.id = containerId;
sec.className = 'mt-4';
var root = document.getElementById('view-radar').querySelector('.radar-grid');
root.appendChild(sec);
parent = sec;
}
if (!clusters.length) { parent.innerHTML = '<div class="text-sm text-gray-500">暂无 >=40 dBZ 云团</div>'; return; }
var html = '<div class="text-sm text-gray-700 mb-2">云团dBZ≥40共 ' + clusters.length + ' 个</div>';
html += '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">';
clusters.forEach(function(cl){
var png = cl.png ? (host + cl.png) : '';
html += '<div class="border border-gray-200 rounded p-2">';
if (png) {
html += '<div class="mb-2 flex items-center justify-center" style="background:#fafafa">'
+ '<img src="'+png+'" style="image-rendering: pixelated; max-width: 100%; max-height: 120px;" />'
+ '</div>';
}
html += '<div class="text-xs text-gray-600">'
+ 'ID: '+cl.id+' | 像元: '+cl.area_px+'<br/>'
+ '质心: '+cl.lon.toFixed(4)+', '+cl.lat.toFixed(4)+'<br/>'
+ 'dBZ: max '+cl.max_dbz.toFixed(1)+' / avg '+cl.avg_dbz.toFixed(1)
+ '</div>';
if (cl.samples && cl.samples.length) {
html += '<div class="mt-1 text-xs text-gray-600">采样点: ' + cl.samples.map(function(s){
return s.role+':('+s.lon.toFixed(3)+','+s.lat.toFixed(3)+')';
}).join(' | ') + '</div>';
}
html += '</div>';
});
html += '</div>';
parent.innerHTML = html;
}).catch(function(){ /* ignore */ });
}
function renderWindQueryList(){
fetch('/api/radar/latest').then(r=>r.json()).then(function(resp){
var meta = resp.meta || {};
var params = meta.query_params || {};
var cands = meta.query_candidates || [];
var containerId = 'radar-wind-query';
var parent = document.getElementById(containerId);
if (!parent) {
var sec = document.createElement('div');
sec.id = containerId;
sec.className = 'mt-4';
var root = document.getElementById('view-radar').querySelector('.radar-grid');
root.appendChild(sec);
parent = sec;
}
var html = '<div class="text-sm text-gray-700 mb-2">风场查询参数</div>';
html += '<div class="text-xs text-gray-600 mb-2">'
+ 'min_area_px='+ (params.min_area_px||9)
+ 'strong_dbz_override=' + (params.strong_dbz_override||50)
+ 'max_samples_per_cluster=' + (params.max_samples_per_cluster||5)
+ 'max_candidates_total=' + (params.max_candidates_total||25)
+ '</div>';
if (!cands.length) {
html += '<div class="text-xs text-gray-500">暂无需要查询的采样点</div>';
} else {
html += '<div class="text-sm text-gray-700 mb-1">需要查询的采样点(共 '+cands.length+' 个)</div>';
html += '<ul class="list-disc pl-5 text-xs text-gray-700">';
cands.forEach(function(p){
html += '<li>cluster='+p.cluster_id+' | '+p.role+' | lon='+p.lon.toFixed(4)+', lat='+p.lat.toFixed(4)+'</li>';
});
html += '</ul>';
}
parent.innerHTML = html;
}).catch(function(){});
}
function renderWindResults(){
fetch('/api/radar/latest/wind').then(r=>r.json()).then(function(resp){
var station = resp.station || {};
var cands = resp.candidates || [];
var clusters = resp.clusters || [];
var containerId = 'radar-wind-results';
var parent = document.getElementById(containerId);
if (!parent) {
var sec = document.createElement('div');
sec.id = containerId;
sec.className = 'mt-4';
var root = document.getElementById('view-radar').querySelector('.radar-grid');
root.appendChild(sec);
parent = sec;
}
var html = '<div class="text-sm text-gray-700 mb-2">风场查询结果(彩云 10m 实况)</div>';
// cluster summary
if (clusters.length) {
html += '<div class="text-xs text-gray-700 mb-2">云团汇总:</div>';
html += '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mb-3">';
clusters.forEach(function(cl){
html += '<div class="border border-gray-200 rounded p-2 text-xs text-gray-700">'
+ 'ID '+cl.cluster_id+' | 距离 '+(cl.distance_km||0).toFixed(1)+' km<br/>'
+ '风 '+(cl.speed_ms||0).toFixed(1)+' m/s, 去向 '+(cl.dir_to_deg||0).toFixed(0)+'°<br/>'
+ (cl.coming?('<span class="text-green-700">朝向</span>, ETA '+(cl.eta_min||0).toFixed(1)+' 分钟'):'<span class="text-gray-500">非朝向</span>')
+ '</div>';
});
html += '</div>';
}
// candidate details
if (cands.length) {
html += '<div class="text-xs text-gray-700 mb-2">采样点明细:</div>';
html += '<div class="overflow-x-auto"><table class="min-w-full text-xs text-gray-700"><thead><tr>'
+ '<th class="px-2 py-1 border">cluster</th>'
+ '<th class="px-2 py-1 border">role</th>'
+ '<th class="px-2 py-1 border">lon</th>'
+ '<th class="px-2 py-1 border">lat</th>'
+ '<th class="px-2 py-1 border">spd(m/s)</th>'
+ '<th class="px-2 py-1 border">dir_from(°)</th>'
+ '<th class="px-2 py-1 border">T(°C)</th>'
+ '<th class="px-2 py-1 border">RH</th>'
+ '<th class="px-2 py-1 border">P(hPa)</th>'
+ '<th class="px-2 py-1 border">err</th>'
+ '</tr></thead><tbody>';
cands.forEach(function(p){
var w = p.wind || {};
html += '<tr>'
+ '<td class="px-2 py-1 border">'+p.cluster_id+'</td>'
+ '<td class="px-2 py-1 border">'+p.role+'</td>'
+ '<td class="px-2 py-1 border">'+p.lon.toFixed(4)+'</td>'
+ '<td class="px-2 py-1 border">'+p.lat.toFixed(4)+'</td>'
+ '<td class="px-2 py-1 border">'+(w.speed_ms!=null?w.speed_ms.toFixed(1):'')+'</td>'
+ '<td class="px-2 py-1 border">'+(w.dir_from_deg!=null?w.dir_from_deg.toFixed(0):'')+'</td>'
+ '<td class="px-2 py-1 border">'+(w.temp_c!=null?w.temp_c.toFixed(1):'')+'</td>'
+ '<td class="px-2 py-1 border">'+(w.rh!=null?(w.rh*100).toFixed(0)+'%':'')+'</td>'
+ '<td class="px-2 py-1 border">'+(w.pressure_hpa!=null?w.pressure_hpa.toFixed(1):'')+'</td>'
+ '<td class="px-2 py-1 border">'+(p.error||'')+'</td>'
+ '</tr>';
});
html += '</tbody></table></div>';
}
parent.innerHTML = html;
}).catch(function(){});
}
})();
</script>
<script defer src="/static/js/alpinejs.min.js"></script>
<script src="/static/js/utils.js"></script>
<script src="/static/js/weather-app.js"></script>
<script src="/static/js/weather-chart.js"></script>
<script src="/static/js/weather-table.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>