监控页面增强:温度变化闪绿光、观测N分钟前、更新戳可见
- 每张卡片加"观测 X 分钟前"时间感知 - 温度值变化时卡片边框闪绿光(HTMX afterSwap 事件) - 更新时间戳移入 HTMX 局部刷新区,30s 刷新可见 - CSS 加 obs-age 样式
This commit is contained in:
@@ -5,6 +5,7 @@ pub struct ObsRow {
|
||||
pub temp_c: Option<f64>,
|
||||
#[allow(dead_code)]
|
||||
pub obs_time: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Get recent temperature observations for an ICAO station.
|
||||
@@ -14,7 +15,7 @@ pub fn get_recent_obs(db_path: &str, icao: &str, minutes: i32, limit: usize) ->
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT temp_c, obs_time FROM airport_obs_log \
|
||||
"SELECT temp_c, obs_time, created_at FROM airport_obs_log \
|
||||
WHERE icao = ?1 AND created_at > datetime('now', '{} minutes') \
|
||||
ORDER BY created_at DESC LIMIT ?2",
|
||||
-minutes
|
||||
@@ -28,6 +29,7 @@ pub fn get_recent_obs(db_path: &str, icao: &str, minutes: i32, limit: usize) ->
|
||||
Ok(ObsRow {
|
||||
temp_c: row.get(0)?,
|
||||
obs_time: row.get(1)?,
|
||||
created_at: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -59,6 +59,18 @@ fn load_city_snapshot(db_path: &str, (key, display, icao, airport, tz, thresh):
|
||||
let temps: Vec<f64> = obs.iter().filter_map(|o| o.temp_c).collect();
|
||||
let current_temp = temps.first().copied();
|
||||
|
||||
// Age of most recent observation
|
||||
let obs_age_min = obs.first().and_then(|o| {
|
||||
o.created_at.as_ref().and_then(|ts| {
|
||||
chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S")
|
||||
.ok()
|
||||
.map(|dt| {
|
||||
let obs_utc = dt.and_utc();
|
||||
(now_utc - obs_utc).num_minutes().max(0)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Trend from last 6 points
|
||||
let trend_data: Vec<f64> = temps.iter().take(6).copied().collect();
|
||||
let trend = trend::calc_trend(&trend_data);
|
||||
@@ -94,6 +106,7 @@ fn load_city_snapshot(db_path: &str, (key, display, icao, airport, tz, thresh):
|
||||
temp_ok: false,
|
||||
trend_ok: false,
|
||||
in_window: false,
|
||||
obs_age_min,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct CitySnapshot {
|
||||
pub temp_ok: bool,
|
||||
pub trend_ok: bool,
|
||||
pub in_window: bool,
|
||||
pub obs_age_min: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
|
||||
@@ -172,6 +172,9 @@ body {
|
||||
.runway-label { color: #4a5160; }
|
||||
.runway-temp { color: #7a8290; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ── obs age ── */
|
||||
.obs-age { font-size: 12px; color: #5a6170; }
|
||||
|
||||
/* ── HTMX indicator ── */
|
||||
.htmx-indicator {
|
||||
text-align: center;
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
<div class="page">
|
||||
<header class="header">
|
||||
<h1>🔥 市场监控</h1>
|
||||
<span class="updated">更新 {{ generated_at }}</span>
|
||||
<span class="updated" id="update-time">更新中…</span>
|
||||
</header>
|
||||
{% endif %}
|
||||
|
||||
<div id="card-grid" class="card-grid"
|
||||
hx-get="/api/data" hx-trigger="every 30s" hx-swap="outerHTML" hx-indicator="#spinner">
|
||||
<span id="update-time" class="updated" style="display:block;grid-column:1/-1;text-align:right;margin-bottom:2px">
|
||||
{{ generated_at }}</span>
|
||||
{% for c in cities %}
|
||||
<div class="card">
|
||||
<div class="card-top">
|
||||
@@ -52,6 +54,15 @@
|
||||
<span class="label">趋势</span>
|
||||
<span class="value trend {{ c.trend.css_class() }}">{{ c.trend.symbol() }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="label">观测</span>
|
||||
{% match c.obs_age_min %}
|
||||
{% when Some with (m) %}
|
||||
<span class="value obs-age">{{ m }} 分钟前</span>
|
||||
{% when None %}
|
||||
<span class="value na">--</span>
|
||||
{% endmatch %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if !c.runway_pairs.is_empty() %}
|
||||
@@ -73,6 +84,29 @@
|
||||
|
||||
{% if full_page %}
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<script>
|
||||
// flash updated cards whose temp changed
|
||||
(function(){
|
||||
let prev = {};
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt){
|
||||
if(evt.detail.target.id !== 'card-grid') return;
|
||||
document.querySelectorAll('.card').forEach(function(card){
|
||||
var name = card.querySelector('.city-name').textContent;
|
||||
var tv = card.querySelector('.temp-value').textContent;
|
||||
var old = prev[name];
|
||||
prev[name] = tv;
|
||||
if(old && old !== tv && tv !== '--'){
|
||||
card.style.transition = 'none';
|
||||
card.style.boxShadow = '0 0 12px rgba(52,211,153,0.35)';
|
||||
requestAnimationFrame(function(){
|
||||
card.style.transition = 'box-shadow 2s ease-out';
|
||||
card.style.boxShadow = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user