diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 8ea9221..a5c4773 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 1b6eaa7..7a1ed85 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 1b6eaa7..7a1ed85 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 7ab2f9b..a657736 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/static/instance_theme.js b/static/instance_theme.js index 1e937ce..8c64bb6 100644 --- a/static/instance_theme.js +++ b/static/instance_theme.js @@ -281,6 +281,7 @@ function apply() { if (!mq.matches) return; + if (consumeSoftNavPending()) return; document.querySelectorAll(".top-nav").forEach(scrollActiveTab); } @@ -329,18 +330,291 @@ } } - /** 顶栏软导航:fetch + document.write 换页,避免整页卸载白屏(实例 standalone / 中控 iframe 均启用) */ - function injectNavTransitionGuard(html, theme) { + function remapInlineStylesInHtml(html) { + if (!html) return html; + return html.replace(/\sstyle=(["'])([\s\S]*?)\1/gi, (full, q, style) => { + return ` style=${q}${remapInlineStyle(style, "light")}${q}`; + }); + } + + function prepareSoftNavHtml(html, theme) { const t = normalize(theme || get()); const bg = META[t]; - const guard = ``; - if (html.includes("")) { - return html.replace("", `${guard}`); + let out = html || ""; + if (t === "light") { + out = remapInlineStylesInHtml(out); } - return guard + html; + const guard = ``; + if (out.includes("")) { + out = out.replace("", `${guard}`); + } else { + out = guard + out; + } + out = out.replace(/]*)>/i, (m, attrs) => { + if (/data-theme=/i.test(attrs)) { + return m.replace(/data-theme="[^"]*"/i, `data-theme="${t}"`); + } + return ``; + }); + const overlay = ``; + if (/]*>/i.test(out)) { + out = out.replace(/]*)>/i, `${overlay}`); + } + return out; + } + + function ensureNavOverlay(theme) { + const t = normalize(theme || get()); + const bg = META[t]; + let el = document.getElementById("inst-nav-overlay"); + if (!el) { + el = document.createElement("div"); + el.id = "inst-nav-overlay"; + el.setAttribute("aria-hidden", "true"); + (document.body || document.documentElement).appendChild(el); + } + el.style.cssText = `position:fixed;inset:0;z-index:2147483647;background:${bg};pointer-events:none;opacity:1;`; + return el; + } + + function dismissNavOverlay() { + const el = document.getElementById("inst-nav-overlay"); + if (!el) return; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + el.style.transition = "opacity 90ms ease"; + el.style.opacity = "0"; + window.setTimeout(() => { + try { + el.remove(); + } catch (_) {} + }, 120); + }); + }); + } + + function markSoftNavPending() { + try { + sessionStorage.setItem("inst-soft-nav", "1"); + } catch (_) {} + } + + function consumeSoftNavPending() { + try { + if (sessionStorage.getItem("inst-soft-nav") === "1") { + sessionStorage.removeItem("inst-soft-nav"); + return true; + } + } catch (_) {} + return false; + } + + const PAGE_CACHE_PREFIX = "inst-pc:v1:"; + const PAGE_CACHE_DAYS_KEY = "inst-page-cache-days"; + const PAGE_CACHE_INDEX_KEY = "inst-page-cache-index"; + const DEFAULT_PAGE_CACHE_DAYS = 7; + const MAX_PAGE_CACHE_ENTRIES = 16; + const CACHE_REVALIDATE_KEY = "inst-cache-revalidate"; + + function getPageCacheMaxAgeMs() { + try { + const days = parseInt(localStorage.getItem(PAGE_CACHE_DAYS_KEY) || "", 10); + if (Number.isFinite(days) && days > 0 && days <= 30) { + return days * 86400000; + } + } catch (_) {} + return DEFAULT_PAGE_CACHE_DAYS * 86400000; + } + + function setPageCacheDays(days) { + const d = parseInt(days, 10); + if (!Number.isFinite(d) || d <= 0 || d > 30) return DEFAULT_PAGE_CACHE_DAYS; + try { + localStorage.setItem(PAGE_CACHE_DAYS_KEY, String(d)); + } catch (_) {} + return d; + } + + function pageCacheStorageKey(cacheKey) { + return PAGE_CACHE_PREFIX + cacheKey; + } + + function pageCacheKey(href) { + try { + const u = new URL(href, location.href); + return `${u.pathname}${u.search}|t=${get()}`; + } catch (_) { + return `${href}|t=${get()}`; + } + } + + function readPageCacheIndex() { + try { + const raw = localStorage.getItem(PAGE_CACHE_INDEX_KEY); + const data = raw ? JSON.parse(raw) : null; + return Array.isArray(data) ? data : []; + } catch (_) { + return []; + } + } + + function writePageCacheIndex(rows) { + try { + localStorage.setItem(PAGE_CACHE_INDEX_KEY, JSON.stringify(rows.slice(0, MAX_PAGE_CACHE_ENTRIES))); + } catch (_) {} + } + + function prunePageCache(aggressive) { + const maxAge = getPageCacheMaxAgeMs(); + const now = Date.now(); + let index = readPageCacheIndex().filter((row) => { + if (!row || !row.key || !row.savedAt) return false; + if (now - row.savedAt > maxAge) { + try { + localStorage.removeItem(pageCacheStorageKey(row.key)); + } catch (_) {} + return false; + } + return true; + }); + index.sort((a, b) => (b.savedAt || 0) - (a.savedAt || 0)); + if (aggressive || index.length > MAX_PAGE_CACHE_ENTRIES) { + index.slice(MAX_PAGE_CACHE_ENTRIES).forEach((row) => { + try { + localStorage.removeItem(pageCacheStorageKey(row.key)); + } catch (_) {} + }); + index = index.slice(0, MAX_PAGE_CACHE_ENTRIES); + } + writePageCacheIndex(index); + } + + function readPageCache(cacheKey) { + prunePageCache(false); + try { + const raw = localStorage.getItem(pageCacheStorageKey(cacheKey)); + if (!raw) return null; + const entry = JSON.parse(raw); + if (!entry || typeof entry.html !== "string" || !entry.savedAt) return null; + if (Date.now() - entry.savedAt > getPageCacheMaxAgeMs()) { + localStorage.removeItem(pageCacheStorageKey(cacheKey)); + return null; + } + return entry; + } catch (_) { + return null; + } + } + + function savePageCache(cacheKey, html) { + if (!html) return; + const savedAt = Date.now(); + const payload = JSON.stringify({ html, savedAt, key: cacheKey }); + try { + localStorage.setItem(pageCacheStorageKey(cacheKey), payload); + } catch (_) { + prunePageCache(true); + try { + localStorage.setItem(pageCacheStorageKey(cacheKey), payload); + } catch (e2) { + return; + } + } + let index = readPageCacheIndex().filter((row) => row && row.key !== cacheKey); + index.unshift({ key: cacheKey, savedAt }); + writePageCacheIndex(index); + } + + function clearPageCache() { + readPageCacheIndex().forEach((row) => { + if (!row || !row.key) return; + try { + localStorage.removeItem(pageCacheStorageKey(row.key)); + } catch (_) {} + }); + try { + localStorage.removeItem(PAGE_CACHE_INDEX_KEY); + } catch (_) {} + } + + function markCacheRevalidatePending() { + try { + sessionStorage.setItem(CACHE_REVALIDATE_KEY, "1"); + } catch (_) {} + } + + function consumeCacheRevalidatePending() { + try { + if (sessionStorage.getItem(CACHE_REVALIDATE_KEY) === "1") { + sessionStorage.removeItem(CACHE_REVALIDATE_KEY); + return true; + } + } catch (_) {} + return false; + } + + function refreshInstanceLiveData() { + const fns = [ + "refreshAccountSnapshot", + "refreshPriceSnapshotConditional", + "refreshPriceSnapshot", + "refreshOrderDefaults", + "loadJournals", + "loadReviews", + ]; + fns.forEach((name) => { + const fn = global[name]; + if (typeof fn === "function") { + try { + fn(); + } catch (_) {} + } + }); + try { + document.dispatchEvent(new CustomEvent("instance-page-revalidate")); + } catch (_) {} + } + + function scheduleLiveDataRefresh() { + window.setTimeout(refreshInstanceLiveData, 0); + window.setTimeout(refreshInstanceLiveData, 350); + } + + function updateSoftNavHistory(href, opts) { + let path = href; + try { + const u = new URL(href, location.href); + path = u.pathname + u.search + u.hash; + } catch (_) {} + if (opts && opts.replace) history.replaceState(null, "", path); + else history.pushState(null, "", path); + } + + function paintSoftNavDocument(html, href, opts) { + updateSoftNavHistory(href, opts); + document.open(); + document.write(html); + document.close(); + } + + function seedCurrentPageCache() { + if (global.__instPageCacheSeeded) return; + global.__instPageCacheSeeded = true; + const href = location.pathname + location.search; + const cacheKey = pageCacheKey(href); + if (readPageCache(cacheKey)) return; + window.setTimeout(() => { + fetch(href, { credentials: "same-origin", cache: "no-store" }) + .then((r) => (r.ok ? r.text() : null)) + .then((html) => { + if (html) savePageCache(cacheKey, html); + }) + .catch(() => {}); + }, 1500); } function initInstanceTopNavSoft() { + prunePageCache(false); let navToken = 0; function isSoftNavLink(a) { @@ -352,27 +626,35 @@ async function navigateInFrame(href, opts) { const token = ++navToken; const themeNow = get(); + const cacheKey = pageCacheKey(href); + ensureNavOverlay(themeNow); + markSoftNavPending(); + + const cached = readPageCache(cacheKey); + let paintedFromCache = false; + if (cached && cached.html) { + markCacheRevalidatePending(); + paintSoftNavDocument(prepareSoftNavHtml(cached.html, themeNow), href, opts); + paintedFromCache = true; + } + try { - const r = await fetch(href, { credentials: "same-origin" }); + const r = await fetch(href, { credentials: "same-origin", cache: "no-store" }); if (token !== navToken) return; if (!r.ok) { - location.href = href; + if (!paintedFromCache) location.href = href; return; } - const html = injectNavTransitionGuard(await r.text(), themeNow); + const rawHtml = await r.text(); + savePageCache(cacheKey, rawHtml); if (token !== navToken) return; - let path = href; - try { - const u = new URL(href, location.href); - path = u.pathname + u.search + u.hash; - } catch (_) {} - if (opts && opts.replace) history.replaceState(null, "", path); - else history.pushState(null, "", path); - document.open(); - document.write(html); - document.close(); + if (!paintedFromCache) { + paintSoftNavDocument(prepareSoftNavHtml(rawHtml, themeNow), href, opts); + } else { + scheduleLiveDataRefresh(); + } } catch (_) { - if (token === navToken) location.href = href; + if (!paintedFromCache && token === navToken) location.href = href; } } @@ -415,6 +697,7 @@ apply(getStandalone()); } initInstanceTopNavSoft(); + seedCurrentPageCache(); function observeDynamicLists() { ["journal-list", "review-list"].forEach((id) => { @@ -438,6 +721,20 @@ syncInlineStyles(get()); patchHubNavLinks(get()); observeDynamicLists(); + if (consumeCacheRevalidatePending()) { + scheduleLiveDataRefresh(); + } + if (document.getElementById("inst-nav-overlay")) { + dismissNavOverlay(); + window.setTimeout(() => { + const el = document.getElementById("inst-nav-overlay"); + if (el) { + try { + el.remove(); + } catch (_) {} + } + }, 800); + } }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", onReady); @@ -468,5 +765,9 @@ mergeHubQueryIntoHref, syncReviewEditButtons, initReviewEditModeSync, + setPageCacheDays, + clearPageCache, + getPageCacheMaxAgeMs, + refreshInstanceLiveData, }; })(typeof window !== "undefined" ? window : globalThis); diff --git a/static/instance_theme_early.css b/static/instance_theme_early.css index f67270b..1713bea 100644 --- a/static/instance_theme_early.css +++ b/static/instance_theme_early.css @@ -41,3 +41,10 @@ html[data-theme="light"] .stat-item { background: #fff !important; border-color: #b8c8d8 !important; } + +#inst-nav-overlay { + position: fixed; + inset: 0; + z-index: 2147483647; + pointer-events: none; +}