From b5f66a0db202e8795a1d93f43d25ee1321a166b3 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 3 Jun 2026 21:40:29 +0800 Subject: [PATCH] feat(hub): show cached monitor board when returning from market area Co-authored-by: Cursor --- manual_trading_hub/static/app.css | 11 +++ manual_trading_hub/static/app.js | 128 +++++++++++++++++++++++---- manual_trading_hub/static/index.html | 2 +- 3 files changed, 121 insertions(+), 20 deletions(-) diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index cb243f5..18e7fc6 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -160,6 +160,17 @@ a:hover { background: rgba(255, 77, 109, 0.1); } +.sys-pill.syncing { + opacity: 0.85; + animation: sys-pill-pulse 1.2s ease-in-out infinite; +} + +@keyframes sys-pill-pulse { + 50% { + opacity: 0.55; + } +} + .top-nav { display: flex; gap: 4px; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index e75b212..91d811a 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -6,6 +6,10 @@ let tpslPending = null; let lastMonitorRows = []; let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || ""; + const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1"; + const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000; + let monitorBoardFetchSeq = 0; + let lastMonitorBoardUpdatedAt = ""; async function apiFetch(url, opts) { const r = await fetch(url, opts); @@ -344,11 +348,78 @@ monitorTimer = null; } + function saveMonitorBoardCache(rows, updatedAt) { + try { + sessionStorage.setItem( + HUB_MONITOR_BOARD_CACHE_KEY, + JSON.stringify({ + version: 1, + updated_at: updatedAt || "", + rows: rows || [], + saved_at: Date.now(), + }) + ); + } catch (_) {} + } + + function loadMonitorBoardFromCache() { + try { + const raw = sessionStorage.getItem(HUB_MONITOR_BOARD_CACHE_KEY); + if (!raw) return null; + const data = JSON.parse(raw); + if (!data || !Array.isArray(data.rows) || !data.rows.length) return null; + const age = Date.now() - Number(data.saved_at || 0); + if (!Number.isFinite(age) || age > HUB_MONITOR_CACHE_MAX_AGE_MS) { + sessionStorage.removeItem(HUB_MONITOR_BOARD_CACHE_KEY); + return null; + } + return data; + } catch (_) { + return null; + } + } + + function restoreMonitorBoardFromCache() { + const cached = loadMonitorBoardFromCache(); + if (!cached) return false; + lastMonitorRows = cached.rows; + lastMonitorBoardUpdatedAt = cached.updated_at || ""; + applyMonitorBoardUi(cached.rows, lastMonitorBoardUpdatedAt, { stale: true }); + return true; + } + + function applyMonitorBoardUi(rows, updatedAt, opts) { + const options = opts || {}; + const tsRaw = updatedAt || lastMonitorBoardUpdatedAt || ""; + if (updatedAt) lastMonitorBoardUpdatedAt = updatedAt; + const online = (rows || []).filter((x) => x.http_ok && (x.agent || {}).ok !== false).length; + const pill = document.getElementById("sys-status"); + if (pill) { + pill.textContent = rows.length ? `LINK ${online}/${rows.length}` : "NO DATA"; + pill.classList.toggle("warn", rows.length && online < rows.length); + if (options.stale) pill.classList.add("syncing"); + else pill.classList.remove("syncing"); + } + const upd = document.getElementById("monitor-updated"); + if (upd) { + const ts = tsRaw.replace("T", " "); + upd.textContent = options.stale + ? ts + ? `缓存 ${ts} · 刷新中…` + : "刷新中…" + : ts + ? `UPD ${ts}` + : ""; + } + renderMonitorGrid(rows || []); + } + function startMonitorPoll() { stopMonitorPoll(); - loadMonitorBoard(); + const hadCache = restoreMonitorBoardFromCache(); + loadMonitorBoard({ background: hadCache }); if (document.getElementById("auto-monitor").checked) { - monitorTimer = setInterval(loadMonitorBoard, 5000); + monitorTimer = setInterval(() => loadMonitorBoard({ background: true }), 5000); } } @@ -502,32 +573,33 @@ return `已保本`; } - async function loadMonitorBoard() { + async function loadMonitorBoard(opts) { + const options = opts || {}; + const background = !!options.background; const box = document.getElementById("monitor-grid"); - const showLoading = !lastMonitorRows.length; + const seq = ++monitorBoardFetchSeq; + const showLoading = !background && !lastMonitorRows.length; if (showLoading && box) { box.innerHTML = '
正在聚合四所数据…
'; + } else if (background && lastMonitorRows.length) { + applyMonitorBoardUi(lastMonitorRows, null, { stale: true }); } try { const r = await apiFetch("/api/monitor/board"); const data = await r.json(); + if (seq !== monitorBoardFetchSeq) return; lastMonitorRows = data.rows || []; - const online = lastMonitorRows.filter( - (x) => x.http_ok && (x.agent || {}).ok !== false - ).length; - const pill = document.getElementById("sys-status"); - if (pill) { - pill.textContent = lastMonitorRows.length - ? `LINK ${online}/${lastMonitorRows.length}` - : "NO DATA"; - pill.classList.toggle("warn", lastMonitorRows.length && online < lastMonitorRows.length); - } - document.getElementById("monitor-updated").textContent = - "UPD " + (data.updated_at || "").replace("T", " "); - renderMonitorGrid(lastMonitorRows); + saveMonitorBoardCache(lastMonitorRows, data.updated_at); + applyMonitorBoardUi(lastMonitorRows, data.updated_at, { stale: false }); } catch (e) { - box.innerHTML = `
${esc(e)}
`; + if (seq !== monitorBoardFetchSeq) return; + if (background && lastMonitorRows.length) { + showToast("监控数据刷新失败,仍显示上次缓存", true); + applyMonitorBoardUi(lastMonitorRows, null, { stale: false }); + return; + } + if (box) box.innerHTML = `
${esc(e)}
`; } } @@ -1826,12 +1898,30 @@ initFullscreen(); initMobileLayout(); + function initShellNav() { + document.querySelectorAll(".top-nav a[href^='/']").forEach((a) => { + a.addEventListener("click", (ev) => { + const href = a.getAttribute("href"); + if (!href || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return; + ev.preventDefault(); + const path = href.split("?")[0]; + if (path === window.location.pathname) { + setActiveNav(); + return; + } + history.pushState({}, "", href); + setActiveNav(); + }); + }); + window.addEventListener("popstate", setActiveNav); + } + initAuth().then((ok) => { if (!ok) return; + initShellNav(); setActiveNav(); if (currentPage() === "settings") { loadSettings().catch(() => {}); } - window.addEventListener("popstate", setActiveNav); }); })(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 04b2830..533c07e 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -245,6 +245,6 @@
- +