From b18b2143b5350e75f6cc2254c0de526b39628f97 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 00:32:03 +0800 Subject: [PATCH] Restore hub iframe soft nav to cut blank tab switch gap. Use fetch in-frame navigation with overlay and hover prefetch; show delayed hub loading spinner instead of hiding the iframe. Co-authored-by: Cursor --- crypto_monitor_binance/templates/index.html | 2 +- crypto_monitor_gate/templates/index.html | 2 +- crypto_monitor_gate_bot/templates/index.html | 2 +- crypto_monitor_okx/templates/index.html | 2 +- manual_trading_hub/static/app.css | 48 +++++- manual_trading_hub/static/app.js | 19 ++- manual_trading_hub/static/index.html | 8 +- static/instance_theme.js | 148 ++++++++++++++++++- 8 files changed, 216 insertions(+), 15 deletions(-) diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index b1bddc6..2bfa979 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 0eb6b4b..606563d 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 0eb6b4b..606563d 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 4a52c24..30ce303 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -3,7 +3,7 @@ - + diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index dd12c7f..6bd02e0 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1183,6 +1183,7 @@ body.market-chart-fs-open { display: flex; flex-direction: column; background: var(--bg, #0a0e14); + isolation: isolate; } .instance-frame-shell.hidden { @@ -1190,7 +1191,52 @@ body.market-chart-fs-open { } .instance-frame-shell.is-instance-nav-loading .instance-frame { - visibility: hidden; + pointer-events: none; +} + +.instance-frame-loading { + display: none; + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 49px; + z-index: 2; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--bg, #0a0e14) 72%, transparent); + color: var(--muted, #8892b0); + font-size: 0.9rem; + pointer-events: none; +} + +.instance-frame-shell.is-instance-nav-loading .instance-frame-loading { + display: flex; +} + +.instance-frame-loading-inner { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: 999px; + border: 1px solid var(--border-soft); + background: color-mix(in srgb, var(--panel-solid) 88%, transparent); +} + +.instance-frame-spinner { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid color-mix(in srgb, var(--muted, #8892b0) 35%, transparent); + border-top-color: var(--accent, #6eb5ff); + animation: instance-frame-spin 0.75s linear infinite; +} + +@keyframes instance-frame-spin { + to { + transform: rotate(360deg); + } } .instance-frame-toolbar { diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index d879461..205cc46 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -429,9 +429,24 @@ return j.url; } + /** @type {number | null} */ + let instanceFrameNavLoadingTimer = null; + function setInstanceFrameNavLoading(loading) { const shell = document.getElementById("instance-frame-shell"); - if (shell) shell.classList.toggle("is-instance-nav-loading", !!loading); + if (!shell) return; + if (instanceFrameNavLoadingTimer != null) { + clearTimeout(instanceFrameNavLoadingTimer); + instanceFrameNavLoadingTimer = null; + } + if (loading) { + instanceFrameNavLoadingTimer = window.setTimeout(() => { + shell.classList.add("is-instance-nav-loading"); + instanceFrameNavLoadingTimer = null; + }, 140); + return; + } + shell.classList.remove("is-instance-nav-loading"); } async function openInstance(exchangeId, nextPath, opts) { @@ -3354,7 +3369,7 @@ if (!d || typeof d !== "object") return; if (d.type === "instance-frame-navigating") { setInstanceFrameNavLoading(true); - } else if (d.type === "instance-frame-ready" || d.type === "instance-theme-ready") { + } else if (d.type === "instance-frame-ready") { setInstanceFrameNavLoading(false); } }); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 9504da0..a4c262d 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -561,6 +561,12 @@ + @@ -1040,6 +1046,6 @@ - + diff --git a/static/instance_theme.js b/static/instance_theme.js index 8923243..2e51854 100644 --- a/static/instance_theme.js +++ b/static/instance_theme.js @@ -338,24 +338,137 @@ function notifyParentFrameReady() { if (!isHubLinked()) return; + dismissNavOverlay(); try { window.parent.postMessage({ type: "instance-frame-ready", theme: get() }, "*"); } catch (_) {} } - /** 中控 iframe:顶栏用正常跳转(与单独打开实例一致),由中控 shell 遮罩盖住 iframe 加载过程。 */ + function ensureNavOverlay() { + const t = normalize(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:2147483646;background:" + + bg + + ";opacity:1;pointer-events:auto;transition:opacity 80ms ease;"; + return el; + } + + function dismissNavOverlay() { + const el = document.getElementById("inst-nav-overlay"); + if (!el) return; + el.style.opacity = "0"; + window.setTimeout(() => { + try { + el.remove(); + } catch (_) {} + }, 90); + } + + function injectNavOverlayIntoHtml(html, theme) { + const t = normalize(theme || get()); + const bg = META[t]; + let out = 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; + } + + /** 中控 iframe:fetch 换页 + 页内遮罩,避免整页卸载与中控侧长时间空白。 */ function initHubEmbedInFrameNav() { if (!isHubLinked()) return; + let navToken = 0; + const prefetch = new Map(); + const PREFETCH_MAX = 8; + function isSoftNavLink(a) { if (!a || !a.getAttribute) return false; if (a.hasAttribute("download") || a.target === "_blank") return false; return !!a.closest(".top-nav, .strategy-subnav"); } - function navigateTopNav(href) { + function rememberPrefetch(href, html) { + if (!href || !html) return; + if (prefetch.has(href)) prefetch.delete(href); + prefetch.set(href, html); + while (prefetch.size > PREFETCH_MAX) { + const first = prefetch.keys().next().value; + prefetch.delete(first); + } + } + + function warmPrefetch(href) { + if (!href || prefetch.has(href)) return; + const token = navToken; + fetch(href, { credentials: "same-origin" }) + .then((r) => (r.ok ? r.text() : null)) + .then((html) => { + if (html && token === navToken) rememberPrefetch(href, html); + }) + .catch(() => {}); + } + + async function navigateInFrame(href, opts) { + const token = ++navToken; notifyParentFrameNavStart(); - location.assign(href); + ensureNavOverlay(); + try { + let html = prefetch.get(href); + if (html) prefetch.delete(href); + if (!html) { + const r = await fetch(href, { credentials: "same-origin" }); + if (token !== navToken) return; + if (!r.ok) { + location.assign(href); + return; + } + html = await r.text(); + } + if (token !== navToken) return; + html = injectNavOverlayIntoHtml(html, get()); + 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(); + } catch (_) { + if (token === navToken) location.assign(href); + } } document.addEventListener( @@ -376,12 +489,30 @@ const nextHref = target.pathname + target.search + target.hash; if (target.pathname === location.pathname && target.search === location.search) return; ev.preventDefault(); - navigateTopNav(nextHref); + void navigateInFrame(nextHref); }, true ); - window.addEventListener("pagehide", notifyParentFrameNavStart); + document.addEventListener( + "pointerenter", + (ev) => { + const a = ev.target.closest("a[href]"); + if (!a || !isSoftNavLink(a)) return; + const rawHref = a.getAttribute("href"); + if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) return; + try { + const target = new URL(rawHref, location.href); + if (target.origin !== location.origin) return; + warmPrefetch(target.pathname + target.search + target.hash); + } catch (_) {} + }, + true + ); + + window.addEventListener("popstate", () => { + void navigateInFrame(location.pathname + location.search + location.hash, { replace: true }); + }); } function purgeLegacySoftNavCache() { @@ -408,7 +539,6 @@ apply(get(), { skipStore: true }); window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); initHubEmbedInFrameNav(); - notifyParentFrameReady(); try { window.parent.postMessage({ type: "instance-theme-ready" }, "*"); } catch (_) {} @@ -438,7 +568,11 @@ syncInlineStyles(get()); patchHubNavLinks(get()); observeDynamicLists(); - if (isHubLinked()) notifyParentFrameReady(); + if (isHubLinked()) { + requestAnimationFrame(() => { + requestAnimationFrame(() => notifyParentFrameReady()); + }); + } }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", onReady);