feat(hub): show cached monitor board when returning from market area
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 `<span class="pos-breakeven-badge">已保本</span>`;
|
||||
}
|
||||
|
||||
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 =
|
||||
'<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在聚合四所数据…</div>';
|
||||
} 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 = `<div class="err">${esc(e)}</div>`;
|
||||
if (seq !== monitorBoardFetchSeq) return;
|
||||
if (background && lastMonitorRows.length) {
|
||||
showToast("监控数据刷新失败,仍显示上次缓存", true);
|
||||
applyMonitorBoardUi(lastMonitorRows, null, { stale: false });
|
||||
return;
|
||||
}
|
||||
if (box) box.innerHTML = `<div class="err">${esc(e)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -245,6 +245,6 @@
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260528-hub-no-30m"></script>
|
||||
<script src="/assets/app.js?v=20260528-hub-tpsl-fix"></script>
|
||||
<script src="/assets/app.js?v=20260603-hub-monitor-cache"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user