Fix hub iframe nav flicker with normal navigation and loading overlay

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 00:06:17 +08:00
parent e3559531d9
commit 7f8ae97a98
7 changed files with 59 additions and 36 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<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=13"></script> <script src="/static/instance_theme.js?v=14"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=3"></script> <script src="/static/account_risk_badge.js?v=3"></script>
+1 -1
View File
@@ -3,7 +3,7 @@
<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=13"></script> <script src="/static/instance_theme.js?v=14"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=3"></script> <script src="/static/account_risk_badge.js?v=3"></script>
+1 -1
View File
@@ -3,7 +3,7 @@
<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=13"></script> <script src="/static/instance_theme.js?v=14"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=3"></script> <script src="/static/account_risk_badge.js?v=3"></script>
+1 -1
View File
@@ -3,7 +3,7 @@
<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=13"></script> <script src="/static/instance_theme.js?v=14"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=3"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=3">
<script src="/static/account_risk_badge.js?v=3"></script> <script src="/static/account_risk_badge.js?v=3"></script>
+4
View File
@@ -1189,6 +1189,10 @@ body.market-chart-fs-open {
display: none !important; display: none !important;
} }
.instance-frame-shell.is-instance-nav-loading .instance-frame {
visibility: hidden;
}
.instance-frame-toolbar { .instance-frame-toolbar {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
+28 -1
View File
@@ -429,6 +429,11 @@
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;
@@ -481,7 +486,10 @@
); );
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);
} }
@@ -498,6 +506,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");
@@ -528,6 +537,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");
} }
@@ -3332,6 +3342,23 @@
const back = document.getElementById("instance-frame-back"); const back = document.getElementById("instance-frame-back");
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");
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) {
+23 -31
View File
@@ -329,41 +329,33 @@
} }
} }
/** 仅中控 iframe 内:fetch + document.write 换页,避免 iframe 整页卸载白屏。单独打开实例仍走浏览器正常跳转。 */ 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", theme: get() }, "*");
} catch (_) {}
}
/** 中控 iframe:顶栏用正常跳转(与单独打开实例一致),由中控 shell 遮罩盖住 iframe 加载过程。 */
function initHubEmbedInFrameNav() { function initHubEmbedInFrameNav() {
if (!isHubLinked()) return; if (!isHubLinked()) return;
let navToken = 0;
function isSoftNavLink(a) { function isSoftNavLink(a) {
if (!a || !a.getAttribute) return false; if (!a || !a.getAttribute) return false;
if (a.hasAttribute("download") || a.target === "_blank") return false;
return !!a.closest(".top-nav, .strategy-subnav"); return !!a.closest(".top-nav, .strategy-subnav");
} }
async function navigateInFrame(href, opts) { function navigateTopNav(href) {
const token = ++navToken; notifyParentFrameNavStart();
try { location.assign(href);
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;
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.href = href;
}
} }
document.addEventListener( document.addEventListener(
@@ -384,14 +376,12 @@
const nextHref = target.pathname + target.search + target.hash; const nextHref = target.pathname + target.search + target.hash;
if (target.pathname === location.pathname && target.search === location.search) return; if (target.pathname === location.pathname && target.search === location.search) return;
ev.preventDefault(); ev.preventDefault();
void navigateInFrame(nextHref); navigateTopNav(nextHref);
}, },
true true
); );
window.addEventListener("popstate", () => { window.addEventListener("pagehide", notifyParentFrameNavStart);
void navigateInFrame(location.pathname + location.search + location.hash, { replace: true });
});
} }
function purgeLegacySoftNavCache() { function purgeLegacySoftNavCache() {
@@ -418,6 +408,7 @@
apply(get(), { skipStore: true }); apply(get(), { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
initHubEmbedInFrameNav(); initHubEmbedInFrameNav();
notifyParentFrameReady();
try { try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*"); window.parent.postMessage({ type: "instance-theme-ready" }, "*");
} catch (_) {} } catch (_) {}
@@ -447,6 +438,7 @@
syncInlineStyles(get()); syncInlineStyles(get());
patchHubNavLinks(get()); patchHubNavLinks(get());
observeDynamicLists(); observeDynamicLists();
if (isHubLinked()) notifyParentFrameReady();
}; };
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onReady); document.addEventListener("DOMContentLoaded", onReady);