监控页面增强:温度变化闪绿光、观测N分钟前、更新戳可见

- 每张卡片加"观测 X 分钟前"时间感知
- 温度值变化时卡片边框闪绿光(HTMX afterSwap 事件)
- 更新时间戳移入 HTMX 局部刷新区,30s 刷新可见
- CSS 加 obs-age 样式
This commit is contained in:
2569718930@qq.com
2026-05-13 19:58:19 +08:00
parent 538cc7db2d
commit 239c9c446d
5 changed files with 55 additions and 2 deletions
+3 -1
View File
@@ -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()
+13
View File
@@ -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,
}
}
+1
View File
@@ -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)]
+3
View File
@@ -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;
+35 -1
View File
@@ -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>