fix(hub): eliminate iframe flash when switching instance nav tabs

Use soft in-frame navigation and loading overlay in hub instance shell; pass embed=1 for iframe SSO opens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-17 19:27:52 +08:00
parent be7f5d5072
commit 6520234bd8
9 changed files with 137 additions and 13 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=6"></script> <script src="/static/instance_theme.js?v=7"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=2">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -2
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=6"></script> <script src="/static/instance_theme.js?v=7"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=2">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -2
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=6"></script> <script src="/static/instance_theme.js?v=7"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=2">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+2 -2
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=6"></script> <script src="/static/instance_theme.js?v=7"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=1"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=2">
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<meta name="apple-mobile-web-app-title" content="监控"> <meta name="apple-mobile-web-app-title" content="监控">
+4
View File
@@ -1092,6 +1092,10 @@ body.market-chart-fs-open {
background: var(--bg); background: var(--bg);
} }
.instance-frame-shell.is-instance-nav-loading .instance-frame {
visibility: hidden;
}
.exchange-fullscreen { .exchange-fullscreen {
position: fixed; position: fixed;
inset: 0; inset: 0;
+31 -3
View File
@@ -371,13 +371,20 @@
return j.url; return j.url;
} }
function setInstanceFrameNavLoading(loading) {
const shell = document.getElementById("instance-frame-shell");
if (shell) shell.classList.toggle("is-instance-nav-loading", !!loading);
}
async function openInstance(exchangeId, nextPath, opts) { async function openInstance(exchangeId, nextPath, opts) {
const options = opts || {}; const options = opts || {};
const newTab = !!options.newTab; const newTab = !!options.newTab;
const next = nextPath || "/"; const next = nextPath || "/";
try { try {
const embedded = isHubEmbedded(); const embedded = isHubEmbedded();
const url = await fetchInstanceOpenUrl(exchangeId, next, { embed: embedded }); const url = await fetchInstanceOpenUrl(exchangeId, next, {
embed: embedded || !newTab,
});
if (newTab) { if (newTab) {
window.open(url, "_blank", "noopener"); window.open(url, "_blank", "noopener");
return; return;
@@ -417,11 +424,14 @@
const url = await fetchInstanceOpenUrl( const url = await fetchInstanceOpenUrl(
instanceFrameCtx.exchangeId, instanceFrameCtx.exchangeId,
instanceFrameCtx.nextPath, instanceFrameCtx.nextPath,
{ embed: isHubEmbedded() } { embed: true }
); );
instanceFrameUrl = url; instanceFrameUrl = url;
const frame = document.getElementById("instance-frame"); const frame = document.getElementById("instance-frame");
if (frame) frame.src = url; if (frame) {
setInstanceFrameNavLoading(true);
frame.src = url;
}
} catch (e) { } catch (e) {
showToast(String(e), true); showToast(String(e), true);
} }
@@ -438,6 +448,7 @@
closeExchangeFullscreen(); closeExchangeFullscreen();
instanceFrameUrl = url; instanceFrameUrl = url;
if (titleEl) titleEl.textContent = title || "实例"; if (titleEl) titleEl.textContent = title || "实例";
setInstanceFrameNavLoading(true);
frame.src = url; frame.src = url;
shell.classList.remove("hidden"); shell.classList.remove("hidden");
shell.setAttribute("aria-hidden", "false"); shell.setAttribute("aria-hidden", "false");
@@ -466,6 +477,7 @@
if (shell) { if (shell) {
shell.classList.add("hidden"); shell.classList.add("hidden");
shell.setAttribute("aria-hidden", "true"); shell.setAttribute("aria-hidden", "true");
shell.classList.remove("is-instance-nav-loading");
} }
document.body.classList.remove("hub-instance-frame-open"); document.body.classList.remove("hub-instance-frame-open");
} }
@@ -3129,6 +3141,22 @@
const refresh = document.getElementById("instance-frame-refresh"); const refresh = document.getElementById("instance-frame-refresh");
const newTab = document.getElementById("instance-frame-newtab"); const newTab = document.getElementById("instance-frame-newtab");
const frame = document.getElementById("instance-frame"); const frame = document.getElementById("instance-frame");
if (frame && frame.dataset.hubNavBound !== "1") {
frame.dataset.hubNavBound = "1";
frame.addEventListener("load", () => setInstanceFrameNavLoading(false));
}
if (!window.__hubInstanceFrameMsgBound) {
window.__hubInstanceFrameMsgBound = true;
window.addEventListener("message", (ev) => {
const d = ev.data;
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") {
setInstanceFrameNavLoading(false);
}
});
}
if (back) back.onclick = () => closeInstanceFrame(); if (back) back.onclick = () => closeInstanceFrame();
if (refresh) refresh.onclick = () => refreshInstanceFrame(); if (refresh) refresh.onclick = () => refreshInstanceFrame();
if (newTab) { if (newTab) {
+2 -2
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260613-host-status-tone" /> <link rel="stylesheet" href="/assets/app.css?v=20260614-instance-frame-nav" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=1" /> <link rel="stylesheet" href="/assets/account_risk_badge.css?v=1" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" /> <link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head> </head>
@@ -654,6 +654,6 @@
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script> <script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script> <script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=2"></script> <script src="/assets/time_close_ui.js?v=2"></script>
<script src="/assets/app.js?v=20260613-host-status-tone"></script> <script src="/assets/app.js?v=20260614-instance-frame-nav"></script>
</body> </body>
</html> </html>
+82
View File
@@ -283,10 +283,92 @@
apply(data.theme, { skipStore: true }); apply(data.theme, { skipStore: true });
} }
function notifyParentFrameNavStart() {
if (!isHubLinked()) return;
try {
window.parent.postMessage({ type: "instance-frame-navigating", theme: get() }, "*");
} catch (_) {}
}
function notifyParentFrameReady() {
if (!isHubLinked()) return;
try {
window.parent.postMessage({ type: "instance-frame-ready" }, "*");
} catch (_) {}
}
/** 中控 iframe 内:拦截顶栏导航,fetch 后 document.write 原地换页,避免 iframe 卸载白屏 */
function initHubEmbedInFrameNav() {
if (!isHubLinked()) return;
let navToken = 0;
function isSoftNavLink(a) {
if (!a || !a.getAttribute) return false;
return !!a.closest(".top-nav, .strategy-subnav");
}
async function navigateInFrame(href, opts) {
const token = ++navToken;
notifyParentFrameNavStart();
try {
const r = await fetch(href, { credentials: "same-origin" });
if (token !== navToken) return;
if (!r.ok) {
location.href = href;
return;
}
const html = await r.text();
if (token !== navToken) return;
document.open();
document.write(html);
document.close();
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);
} catch (_) {
if (token === navToken) location.href = href;
}
}
document.addEventListener(
"click",
(ev) => {
const a = ev.target.closest("a[href]");
if (!a || !isSoftNavLink(a) || ev.defaultPrevented) return;
if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
const rawHref = a.getAttribute("href");
if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) return;
let target;
try {
target = new URL(rawHref, location.href);
} catch (_) {
return;
}
if (target.origin !== location.origin) return;
const nextHref = target.pathname + target.search + target.hash;
if (target.pathname === location.pathname && target.search === location.search) return;
ev.preventDefault();
void navigateInFrame(nextHref);
},
true
);
window.addEventListener("popstate", () => {
void navigateInFrame(location.pathname + location.search + location.hash, { replace: true });
});
}
function boot() { function boot() {
if (isHubLinked()) { if (isHubLinked()) {
apply(get(), { skipStore: true }); apply(get(), { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
initHubEmbedInFrameNav();
notifyParentFrameReady();
try { try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*"); window.parent.postMessage({ type: "instance-theme-ready" }, "*");
} catch (_) {} } catch (_) {}
+10
View File
@@ -1,4 +1,14 @@
/* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */ /* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */
html {
background: #0b0d14;
color-scheme: dark;
}
html[data-theme="light"] {
background: #d8e2ec;
color-scheme: light;
}
html[data-theme="light"] body { html[data-theme="light"] body {
background: #d8e2ec !important; background: #d8e2ec !important;
color: #1a2838 !important; color: #1a2838 !important;