feat: 雷达历史数据
This commit is contained in:
parent
2085fd9a31
commit
a2a7cfd744
@ -103,7 +103,7 @@
|
||||
let gTileValues = null, gXs = null, gYs = null;
|
||||
let gWindFromDeg = null, gWindSpeedMS = null;
|
||||
let gTimes = [];
|
||||
let gCurrentIdx = -1; // 在 gTimes 中的索引(倒序:0=最新)
|
||||
let gCurrentIdx = -1;
|
||||
|
||||
function toRad(d){ return d * Math.PI / 180; }
|
||||
function toDeg(r){ return r * 180 / Math.PI; }
|
||||
@ -172,14 +172,12 @@
|
||||
document.getElementById('east').textContent = fmt5(t.east);
|
||||
document.getElementById('north').textContent = fmt5(t.north);
|
||||
document.getElementById('res').textContent = fmt5(t.res_deg);
|
||||
// 标题时间与索引同步
|
||||
document.getElementById('titleDt').textContent = `(${t.dt})`;
|
||||
const selBox = document.getElementById('timeSelect');
|
||||
for(let i=0;i<selBox.options.length;i++){ if(selBox.options[i].value===t.dt){ selBox.selectedIndex=i; break; } }
|
||||
gCurrentIdx = gTimes.indexOf(t.dt);
|
||||
updateCountAndButtons(); updateSlider();
|
||||
|
||||
// x/y 等角坐标
|
||||
const w=t.width,h=t.height; gTileValues=t.values;
|
||||
const xs=new Array(w), ys=new Array(h);
|
||||
const stepX=(t.east-t.west)/w, stepY=(t.north-t.south)/h;
|
||||
@ -187,7 +185,6 @@
|
||||
for(let j=0;j<h;j++){ ys[j]=t.south+(j+0.5)*stepY; }
|
||||
gXs=xs; gYs=ys;
|
||||
|
||||
// 色带
|
||||
const colors=["#0000FF","#00BFFF","#00FFFF","#7FFFD4","#7CFC00","#ADFF2F","#FFFF00","#FFD700","#FFA500","#FF8C00","#FF4500","#FF0000","#DC143C","#C71585","#8B008B"];
|
||||
const scale=[]; for(let i=0;i<colors.length;i++){ const tpos=colors.length===1?0:i/(colors.length-1); scale.push([tpos,colors[i]]); }
|
||||
const zBins=[]; const custom=[];
|
||||
@ -206,7 +203,6 @@
|
||||
hovertemplate:'lon=%{x:.3f}, lat=%{y:.3f}<br>dBZ=%{customdata[2]:.1f}<extra></extra>' };
|
||||
const layout={ autosize:true, margin:{l:40,r:8,t:8,b:90}, xaxis:{title:'经度',tickformat:'.2f',zeroline:false,constrain:'domain',automargin:true},
|
||||
yaxis:{title:{text:'纬度',standoff:12},tickformat:'.2f',zeroline:false,scaleanchor:'x',scaleratio:1,constrain:'domain',automargin:true} };
|
||||
// 扇形覆盖
|
||||
const data=[heatTrace];
|
||||
if(gWindFromDeg!==null && gWindSpeedMS>0){
|
||||
const half=30, samples=64, start=gWindFromDeg-half, end=gWindFromDeg+half, rangeM=gWindSpeedMS*3*3600;
|
||||
@ -219,7 +215,6 @@
|
||||
Plotly.newPlot(plotEl, data, layout, {responsive:true, displayModeBar:false}).then(()=>{ const s=plotEl.clientWidth; Plotly.relayout(plotEl,{height:s}); });
|
||||
window.addEventListener('resize',()=>{ const s=plotEl.clientWidth; Plotly.relayout(plotEl,{height:s}); Plotly.Plots.resize(plotEl); });
|
||||
|
||||
// 与瓦片时间对齐的10分钟气象
|
||||
try{
|
||||
const base = forcedDt ? new Date(forcedDt.replace(/-/g,'/')) : new Date(t.dt.replace(/-/g,'/'));
|
||||
const ceil10=new Date(base); const m=base.getMinutes(); const up=(Math.floor(m/10)*10+10)%60; ceil10.setMinutes(up,0,0); if(up===0){ ceil10.setHours(base.getHours()+1); }
|
||||
@ -286,7 +281,6 @@
|
||||
try{
|
||||
if(!gTileValues || !gXs || !gYs || gWindSpeedMS===null) return;
|
||||
const R=6371000; const rangeM=gWindSpeedMS*3*3600;
|
||||
// 以站点为中心的正方形(3小时可达半径),作为“细节放大”裁剪窗口
|
||||
const dLat=(rangeM/R)*(180/Math.PI);
|
||||
const dLon=(rangeM/(R*Math.cos(toRad(ST_LAT))))*(180/Math.PI);
|
||||
const latMin=ST_LAT-dLat, latMax=ST_LAT+dLat, lonMin=ST_LON-dLon, lonMax=ST_LON+dLon;
|
||||
@ -308,7 +302,6 @@
|
||||
colorbar:{orientation:'h',x:0.5,y:-0.12,xanchor:'center',yanchor:'top',len:0.8,thickness:16,title:{text:'dBZ',side:'bottom'},
|
||||
tickmode:'array',tickvals:Array.from({length:15},(_,i)=>i),ticktext:Array.from({length:15},(_,i)=>String(i*5))},
|
||||
hovertemplate:'lon=%{x:.3f}, lat=%{y:.3f}<br>dBZ=%{customdata[2]:.1f}<extra></extra>' }];
|
||||
// 扇形覆盖(同样默认显示)
|
||||
if(gWindFromDeg!==null && gWindSpeedMS>0){
|
||||
const half=30, samples=64, start=gWindFromDeg-half, end=gWindFromDeg+half, rangeM=gWindSpeedMS*3*3600;
|
||||
const xsFan=[ST_LON], ysFan=[ST_LAT];
|
||||
@ -326,7 +319,6 @@
|
||||
}catch(e){ document.getElementById('squarePlot').innerHTML=`<div class="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">正方形热力图渲染失败:${e.message}</div>`; }
|
||||
}
|
||||
|
||||
// 启动加载:预填近3小时范围并填充时次
|
||||
(function initRange(){ const end=new Date(); const start=new Date(end.getTime()-3*3600*1000); document.getElementById('tsStart').value=fmtDTLocal(start); document.getElementById('tsEnd').value=fmtDTLocal(end); })();
|
||||
const startStr = fromDTLocalInput(document.getElementById('tsStart').value);
|
||||
const endStr = fromDTLocalInput(document.getElementById('tsEnd').value);
|
||||
@ -340,7 +332,7 @@
|
||||
});
|
||||
document.getElementById('tsQuery').addEventListener('click', async ()=>{ const s=fromDTLocalInput(document.getElementById('tsStart').value); const e=fromDTLocalInput(document.getElementById('tsEnd').value); await populateTimes(s,e); });
|
||||
function updateCountAndButtons(){
|
||||
const N=gTimes.length; const k=gCurrentIdx>=0?(gCurrentIdx+1):0; document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时刻`;
|
||||
const N=gTimes.length; const k=gCurrentIdx>=0?(gCurrentIdx+1):0; document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时次`;
|
||||
const prev=document.getElementById('btnPrev'); const next=document.getElementById('btnNext');
|
||||
prev.disabled = !(N>0 && gCurrentIdx>=0 && gCurrentIdx<N-1);
|
||||
next.disabled = !(N>0 && gCurrentIdx>0);
|
||||
@ -350,7 +342,7 @@
|
||||
const N = gTimes.length;
|
||||
slider.max = N > 0 ? String(N-1) : '0';
|
||||
if (N > 0 && gCurrentIdx >= 0) {
|
||||
const sliderVal = (N - 1) - gCurrentIdx; // 值越大越新
|
||||
const sliderVal = (N - 1) - gCurrentIdx;
|
||||
slider.value = String(sliderVal);
|
||||
}
|
||||
slider.disabled = N === 0;
|
||||
@ -365,7 +357,7 @@
|
||||
const N = gTimes.length; if (N === 0) return;
|
||||
const raw = parseInt(e.target.value, 10);
|
||||
const sliderVal = Math.max(0, Math.min(N-1, isNaN(raw)?0:raw));
|
||||
const idx = (N - 1) - sliderVal; // 反向映射到数组索引(0=最新)
|
||||
const idx = (N - 1) - sliderVal;
|
||||
if (idx === gCurrentIdx) return;
|
||||
gCurrentIdx = idx;
|
||||
const dt = gTimes[gCurrentIdx];
|
||||
|
||||
@ -87,7 +87,6 @@
|
||||
<button id="btnNext" class="px-2 py-1 border rounded">下一时次</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 居中滑动条,联动历史时次(0=最新,值越大越旧) -->
|
||||
<div class="w-full flex justify-center mb-2">
|
||||
<input id="timeSlider" type="range" min="0" max="0" value="0" step="1" class="slider slider-horizontal w-64" />
|
||||
</div>
|
||||
@ -101,9 +100,9 @@
|
||||
|
||||
<script>
|
||||
let gTileValues = null, gXs = null, gYs = null;
|
||||
let gWindFromDeg = null, gWindSpeedMS = null; // 来风方向(度,0=北,顺时针),风速 m/s
|
||||
let gWindFromDeg = null, gWindSpeedMS = null;
|
||||
let gTimes = [];
|
||||
let gCurrentIdx = -1; // 在 gTimes 中的索引(倒序:0=最新)
|
||||
let gCurrentIdx = -1;
|
||||
|
||||
async function loadLatestTile() {
|
||||
const res = await fetch('/api/radar/latest?z=7&y=40&x=102');
|
||||
@ -125,10 +124,8 @@
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||
}
|
||||
function fromDTLocalInput(s){
|
||||
// s like 'YYYY-MM-DDTHH:MM'
|
||||
if(!s) return null;
|
||||
const t = new Date(s.replace('T','-').replace(/-/g,'/')); // iOS safe parse later
|
||||
// We will manually format to server expected 'YYYY-MM-DD HH:MM:SS'
|
||||
const t = new Date(s.replace('T','-').replace(/-/g,'/'));
|
||||
const pad=(n)=> String(n).padStart(2,'0');
|
||||
return `${t.getFullYear()}-${pad(t.getMonth()+1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:00`;
|
||||
}
|
||||
@ -162,7 +159,6 @@
|
||||
async function renderTile(t, forcedDt) {
|
||||
const fmt5 = (n) => Number(n).toFixed(5);
|
||||
|
||||
// 填充元信息
|
||||
document.getElementById('dt').textContent = t.dt;
|
||||
document.getElementById('z').textContent = t.z;
|
||||
document.getElementById('y').textContent = t.y;
|
||||
@ -173,7 +169,6 @@
|
||||
document.getElementById('east').textContent = fmt5(t.east);
|
||||
document.getElementById('north').textContent = fmt5(t.north);
|
||||
document.getElementById('res').textContent = fmt5(t.res_deg);
|
||||
// 标题时间与索引同步
|
||||
document.getElementById('titleDt').textContent = `(${t.dt})`;
|
||||
const selBox = document.getElementById('timeSelect');
|
||||
for (let i = 0; i < selBox.options.length; i++) {
|
||||
@ -182,7 +177,6 @@
|
||||
gCurrentIdx = gTimes.indexOf(t.dt);
|
||||
updateCountAndButtons();
|
||||
|
||||
// Plotly 热力图(离散 5 dBZ 色带)
|
||||
const colors = [
|
||||
"#0000F6","#01A0F6","#00ECEC","#01FF00","#00C800",
|
||||
"#019000","#FFFF00","#E7C000","#FF9000","#FF0000",
|
||||
@ -192,17 +186,14 @@
|
||||
const resDeg = t.res_deg;
|
||||
const west = t.west, south = t.south, north = t.north;
|
||||
|
||||
// 经纬度轴(像元中心)。x 自西向东递增;y 自南向北递增(保证上=北,下=南)。
|
||||
const xs = new Array(w);
|
||||
for (let c = 0; c < w; c++) xs[c] = west + (c + 0.5) * resDeg;
|
||||
const ys = new Array(h);
|
||||
for (let r = 0; r < h; r++) ys[r] = south + (r + 0.5) * resDeg;
|
||||
|
||||
// 缓存用于后续扇形计算
|
||||
gTileValues = t.values;
|
||||
gXs = xs; gYs = ys;
|
||||
|
||||
// 将 dBZ 离散到 0..14 档,并保留原值用于 hover
|
||||
const zBins = new Array(h);
|
||||
const custom = new Array(h);
|
||||
for (let r = 0; r < h; r++) {
|
||||
@ -218,7 +209,6 @@
|
||||
zBins[r] = rowBins; custom[r] = rowCustom;
|
||||
}
|
||||
|
||||
// 颜色刻度(0..14 归一化到 0..1)
|
||||
const scale = [];
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
const tpos = colors.length === 1 ? 0 : i / (colors.length - 1);
|
||||
@ -252,11 +242,10 @@
|
||||
'dBZ=%{customdata[2]:.1f}<extra></extra>'
|
||||
}];
|
||||
|
||||
// 叠加“来风±30°扇形(3小时可达半径)”覆盖层(默认显示、无图例项)到主图
|
||||
if (gWindFromDeg !== null && gWindSpeedMS > 0) {
|
||||
const stLat = 23.097234, stLon = 108.715433;
|
||||
const half = 30; // 半开角
|
||||
const samples = 64; // 弧线采样点
|
||||
const half = 30;
|
||||
const samples = 64;
|
||||
const start = gWindFromDeg - half;
|
||||
const end = gWindFromDeg + half;
|
||||
const rangeM = gWindSpeedMS * 3 * 3600;
|
||||
@ -281,13 +270,11 @@
|
||||
autosize: true,
|
||||
margin: {l:40, r:8, t:8, b:90},
|
||||
xaxis: {title: '经度', tickformat: '.2f', zeroline: false, constrain: 'domain', automargin: true},
|
||||
// y 轴默认从小到大(下→上),配合 ys 自南到北递增,实现“上=北,下=南”
|
||||
yaxis: {title: {text: '纬度', standoff: 12}, tickformat: '.2f', zeroline: false, scaleanchor: 'x', scaleratio: 1, constrain: 'domain', automargin: true},
|
||||
};
|
||||
|
||||
const plotEl = document.getElementById('radarPlot');
|
||||
Plotly.newPlot(plotEl, data, layout, {responsive: true, displayModeBar: false}).then(() => {
|
||||
// 强制按照容器宽度设置高度,保持方形
|
||||
const size = plotEl.clientWidth;
|
||||
Plotly.relayout(plotEl, {height: size});
|
||||
});
|
||||
@ -297,7 +284,6 @@
|
||||
Plotly.Plots.resize(plotEl);
|
||||
});
|
||||
|
||||
// 同步加载与瓦片时间对应的10分钟气象(向上取整到10分)
|
||||
try {
|
||||
const dt = forcedDt ? new Date(forcedDt.replace(/-/g,'/')) : new Date(t.dt.replace(/-/g,'/'));
|
||||
const ceil10 = new Date(dt);
|
||||
@ -308,11 +294,9 @@
|
||||
const pad = (n)=> n.toString().padStart(2,'0');
|
||||
const dtStr = `${ceil10.getFullYear()}-${pad(ceil10.getMonth()+1)}-${pad(ceil10.getDate())} ${pad(ceil10.getHours())}:${pad(ceil10.getMinutes())}:00`;
|
||||
await loadRealtimeAt(dtStr);
|
||||
// 扇形计算 + 正方形裁剪热力图
|
||||
maybeCalcSector();
|
||||
maybePlotSquare();
|
||||
} catch (e) {
|
||||
// 回退到最新
|
||||
await loadRealtimeLatest();
|
||||
maybeCalcSector();
|
||||
maybePlotSquare();
|
||||
@ -339,8 +323,8 @@
|
||||
document.getElementById('rt_wd').textContent = f2(r.wind_direction);
|
||||
document.getElementById('rt_p').textContent = f2(r.pressure);
|
||||
|
||||
gWindFromDeg = Number(r.wind_direction); // 来风方向(度)
|
||||
gWindSpeedMS = Number(r.wind_speed); // m/s(后端已转好)
|
||||
gWindFromDeg = Number(r.wind_direction);
|
||||
gWindSpeedMS = Number(r.wind_speed);
|
||||
}
|
||||
|
||||
async function loadRealtimeAt(dtStr) {
|
||||
@ -370,7 +354,7 @@
|
||||
function toRad(d){ return d * Math.PI / 180; }
|
||||
function toDeg(r){ return r * 180 / Math.PI; }
|
||||
function haversine(lat1, lon1, lat2, lon2){
|
||||
const R = 6371000; // m
|
||||
const R = 6371000;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2;
|
||||
@ -382,15 +366,14 @@
|
||||
const y = Math.sin(Δλ) * Math.cos(φ2);
|
||||
const x = Math.cos(φ1)*Math.sin(φ2) - Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ);
|
||||
let brng = toDeg(Math.atan2(y, x));
|
||||
return (brng + 360) % 360; // 0..360, 0=北,顺时针
|
||||
return (brng + 360) % 360;
|
||||
}
|
||||
function angDiff(a, b){
|
||||
let d = ((a - b + 540) % 360) - 180; // -180..180
|
||||
let d = ((a - b + 540) % 360) - 180;
|
||||
return Math.abs(d);
|
||||
}
|
||||
// 前向大地解算:从(lat,lon)出发,沿方位角brgDeg,距离distM,求目的地经纬度
|
||||
function destPoint(lat, lon, brgDeg, distM){
|
||||
const R = 6371000; // m
|
||||
const R = 6371000;
|
||||
const δ = distM / R;
|
||||
const θ = toRad(brgDeg);
|
||||
const φ1 = toRad(lat);
|
||||
@ -402,19 +385,18 @@
|
||||
const y = Math.sin(θ) * sinδ * cosφ1;
|
||||
const x = cosδ - sinφ1 * sinφ2;
|
||||
const λ2 = λ1 + Math.atan2(y, x);
|
||||
return { lat: toDeg(φ2), lon: ((toDeg(λ2) + 540) % 360) - 180 }; // 归一化到[-180,180)
|
||||
return { lat: toDeg(φ2), lon: ((toDeg(λ2) + 540) % 360) - 180 };
|
||||
}
|
||||
function maybeCalcSector(){
|
||||
try{
|
||||
if(!gTileValues || !gXs || !gYs || gWindFromDeg===null || gWindSpeedMS===null){
|
||||
return; // 等待数据就绪
|
||||
return;
|
||||
}
|
||||
// 站点(南宁雷达站经纬度)
|
||||
const stLat = 23.097234, stLon = 108.715433;
|
||||
const halfAngle = 30; // ±30°
|
||||
const rangeM = gWindSpeedMS * 3 * 3600; // 三小时覆盖半径
|
||||
const halfAngle = 30;
|
||||
const rangeM = gWindSpeedMS * 3 * 3600;
|
||||
|
||||
let best = null; // {dist, lat, lon, dbz}
|
||||
let best = null;
|
||||
const h = gTileValues.length;
|
||||
const w = gTileValues[0].length;
|
||||
for(let r=0; r<h; r++){
|
||||
@ -423,12 +405,11 @@
|
||||
const val = gTileValues[r][c];
|
||||
if(val==null) continue;
|
||||
const dbz = Number(val);
|
||||
if(!(dbz >= 40)) continue; // 门限
|
||||
if(!(dbz >= 40)) continue;
|
||||
const lon = gXs[c];
|
||||
const dist = haversine(stLat, stLon, lat, lon);
|
||||
if(dist > rangeM) continue;
|
||||
const brg = bearingDeg(stLat, stLon, lat, lon);
|
||||
// 以“来风方向”为扇形中心
|
||||
if(angDiff(brg, gWindFromDeg) > halfAngle) continue;
|
||||
if(!best || dist < best.dist){ best = {dist, lat, lon, dbz}; }
|
||||
}
|
||||
@ -457,19 +438,16 @@
|
||||
function maybePlotSquare(){
|
||||
try{
|
||||
if(!gTileValues || !gXs || !gYs || gWindSpeedMS===null){
|
||||
return; // 等待数据
|
||||
return;
|
||||
}
|
||||
// 常量与站点
|
||||
const R = 6371000; // 地球半径(m)
|
||||
const R = 6371000;
|
||||
const stLat = 23.097234, stLon = 108.715433;
|
||||
const rangeM = gWindSpeedMS * 3 * 3600; // 3小时
|
||||
// 米->度(近似):
|
||||
const rangeM = gWindSpeedMS * 3 * 3600;
|
||||
const dLat = (rangeM / R) * (180/Math.PI);
|
||||
const dLon = (rangeM / (R * Math.cos(toRad(stLat)))) * (180/Math.PI);
|
||||
const latMin = stLat - dLat, latMax = stLat + dLat;
|
||||
const lonMin = stLon - dLon, lonMax = stLon + dLon;
|
||||
|
||||
// 定位索引范围(y从南到北递增, x从西到东递增)
|
||||
const h = gYs.length, w = gXs.length;
|
||||
let rStart = 0; while(rStart < h && gYs[rStart] < latMin) rStart++;
|
||||
let rEnd = h-1; while(rEnd >= 0 && gYs[rEnd] > latMax) rEnd--;
|
||||
@ -484,7 +462,6 @@
|
||||
const xs = gXs.slice(cStart, cEnd+1);
|
||||
const ys = gYs.slice(rStart, rEnd+1);
|
||||
|
||||
// 离散到色带(与主图一致)
|
||||
const colors = [
|
||||
"#0000F6","#01A0F6","#00ECEC","#01FF00","#00C800",
|
||||
"#019000","#FFFF00","#E7C000","#FF9000","#FF0000",
|
||||
@ -527,15 +504,13 @@
|
||||
hovertemplate: 'lon=%{x:.3f}, lat=%{y:.3f}<br>dBZ=%{customdata[2]:.1f}<extra></extra>'
|
||||
}];
|
||||
|
||||
// 叠加“来风±30°扇形(3小时可达半径)”覆盖层(默认显示、无图例项)
|
||||
if (gWindFromDeg !== null && gWindSpeedMS > 0) {
|
||||
const half = 30; // 半开角
|
||||
const samples = 64; // 弧线采样点
|
||||
const half = 30;
|
||||
const samples = 64;
|
||||
const start = gWindFromDeg - half;
|
||||
const end = gWindFromDeg + half;
|
||||
const xsFan = [];
|
||||
const ysFan = [];
|
||||
// 从中心起点,沿弧线,再回到中心,闭合多边形
|
||||
xsFan.push(stLon); ysFan.push(stLat);
|
||||
for (let i = 0; i <= samples; i++) {
|
||||
const θ = start + (end - start) * (i / samples);
|
||||
@ -556,7 +531,6 @@
|
||||
xaxis: {title: '经度', tickformat: '.2f', zeroline: false, constrain: 'domain', automargin: true},
|
||||
yaxis: {title: {text: '纬度', standoff: 12}, tickformat: '.2f', zeroline: false, scaleanchor: 'x', scaleratio: 1, constrain: 'domain', automargin: true},
|
||||
shapes: [
|
||||
// 画出正方形边界(lonMin..lonMax, latMin..latMax)
|
||||
{type:'rect', xref:'x', yref:'y', x0: lonMin, x1: lonMax, y0: latMin, y1: latMax, line: {color:'#111', width:1, dash:'dot'}, fillcolor:'rgba(0,0,0,0)'}
|
||||
]
|
||||
};
|
||||
@ -576,8 +550,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:加载最新瓦片、填充时间下拉(默认近3小时)、绑定事件
|
||||
// 预填范围
|
||||
(function initRange(){
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 3*3600*1000);
|
||||
@ -611,7 +583,7 @@
|
||||
function updateCountAndButtons(){
|
||||
const N = gTimes.length;
|
||||
const k = gCurrentIdx >= 0 ? (gCurrentIdx + 1) : 0;
|
||||
document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时刻`;
|
||||
document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时次`;
|
||||
const prev = document.getElementById('btnPrev');
|
||||
const next = document.getElementById('btnNext');
|
||||
prev.disabled = !(N > 0 && gCurrentIdx >= 0 && gCurrentIdx < N-1);
|
||||
@ -641,13 +613,12 @@
|
||||
updateSlider();
|
||||
});
|
||||
|
||||
// 滑动条联动
|
||||
function updateSlider(){
|
||||
const slider = document.getElementById('timeSlider');
|
||||
const N = gTimes.length;
|
||||
slider.max = N > 0 ? String(N-1) : '0';
|
||||
if (N > 0 && gCurrentIdx >= 0) {
|
||||
const sliderVal = (N - 1) - gCurrentIdx; // 值越大越新
|
||||
const sliderVal = (N - 1) - gCurrentIdx;
|
||||
slider.value = String(sliderVal);
|
||||
}
|
||||
slider.disabled = N === 0;
|
||||
@ -656,7 +627,7 @@
|
||||
const N = gTimes.length; if (N === 0) return;
|
||||
const raw = parseInt(e.target.value, 10);
|
||||
const sliderVal = Math.max(0, Math.min(N-1, isNaN(raw)?0:raw));
|
||||
const idx = (N - 1) - sliderVal; // 反向映射到数组索引(0=最新)
|
||||
const idx = (N - 1) - sliderVal;
|
||||
if (idx === gCurrentIdx) return;
|
||||
gCurrentIdx = idx;
|
||||
const dt = gTimes[gCurrentIdx];
|
||||
@ -664,7 +635,6 @@
|
||||
await loadTileAt(dt);
|
||||
updateCountAndButtons();
|
||||
});
|
||||
// 兜底加载最新
|
||||
loadRealtimeLatest().catch(err => {
|
||||
const info = document.getElementById('rtInfo');
|
||||
info.innerHTML = `<div class="text-sm text-red-600">${err.message}</div>`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user