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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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=14"></script>
|
<script src="/static/instance_theme.js?v=15"></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>
|
||||||
|
|||||||
@@ -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=14"></script>
|
<script src="/static/instance_theme.js?v=15"></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>
|
||||||
|
|||||||
@@ -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=14"></script>
|
<script src="/static/instance_theme.js?v=15"></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>
|
||||||
|
|||||||
@@ -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=14"></script>
|
<script src="/static/instance_theme.js?v=15"></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>
|
||||||
|
|||||||
@@ -1183,6 +1183,7 @@ body.market-chart-fs-open {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg, #0a0e14);
|
background: var(--bg, #0a0e14);
|
||||||
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-frame-shell.hidden {
|
.instance-frame-shell.hidden {
|
||||||
@@ -1190,7 +1191,52 @@ body.market-chart-fs-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.instance-frame-shell.is-instance-nav-loading .instance-frame {
|
.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 {
|
.instance-frame-toolbar {
|
||||||
|
|||||||
@@ -429,9 +429,24 @@
|
|||||||
return j.url;
|
return j.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {number | null} */
|
||||||
|
let instanceFrameNavLoadingTimer = null;
|
||||||
|
|
||||||
function setInstanceFrameNavLoading(loading) {
|
function setInstanceFrameNavLoading(loading) {
|
||||||
const shell = document.getElementById("instance-frame-shell");
|
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) {
|
async function openInstance(exchangeId, nextPath, opts) {
|
||||||
@@ -3354,7 +3369,7 @@
|
|||||||
if (!d || typeof d !== "object") return;
|
if (!d || typeof d !== "object") return;
|
||||||
if (d.type === "instance-frame-navigating") {
|
if (d.type === "instance-frame-navigating") {
|
||||||
setInstanceFrameNavLoading(true);
|
setInstanceFrameNavLoading(true);
|
||||||
} else if (d.type === "instance-frame-ready" || d.type === "instance-theme-ready") {
|
} else if (d.type === "instance-frame-ready") {
|
||||||
setInstanceFrameNavLoading(false);
|
setInstanceFrameNavLoading(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -561,6 +561,12 @@
|
|||||||
<button type="button" id="instance-frame-newtab" class="ghost">新标签打开</button>
|
<button type="button" id="instance-frame-newtab" class="ghost">新标签打开</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="instance-frame-loading" class="instance-frame-loading" aria-hidden="true">
|
||||||
|
<div class="instance-frame-loading-inner">
|
||||||
|
<span class="instance-frame-spinner" aria-hidden="true"></span>
|
||||||
|
<span>加载中…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<iframe id="instance-frame" class="instance-frame" title="交易所实例"></iframe>
|
<iframe id="instance-frame" class="instance-frame" title="交易所实例"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1040,6 +1046,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=20260614-calculator"></script>
|
<script src="/assets/app.js?v=20260614-instance-nav"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+141
-7
@@ -338,24 +338,137 @@
|
|||||||
|
|
||||||
function notifyParentFrameReady() {
|
function notifyParentFrameReady() {
|
||||||
if (!isHubLinked()) return;
|
if (!isHubLinked()) return;
|
||||||
|
dismissNavOverlay();
|
||||||
try {
|
try {
|
||||||
window.parent.postMessage({ type: "instance-frame-ready", theme: get() }, "*");
|
window.parent.postMessage({ type: "instance-frame-ready", theme: get() }, "*");
|
||||||
} catch (_) {}
|
} 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 =
|
||||||
|
'<style id="inst-nav-guard">html,body{background:' +
|
||||||
|
bg +
|
||||||
|
"!important;color-scheme:" +
|
||||||
|
t +
|
||||||
|
';}</style>';
|
||||||
|
if (out.includes("</head>")) {
|
||||||
|
out = out.replace("</head>", guard + "</head>");
|
||||||
|
} else {
|
||||||
|
out = guard + out;
|
||||||
|
}
|
||||||
|
out = out.replace(/<html([^>]*)>/i, (m, attrs) => {
|
||||||
|
if (/data-theme=/i.test(attrs)) {
|
||||||
|
return m.replace(/data-theme="[^"]*"/i, 'data-theme="' + t + '"');
|
||||||
|
}
|
||||||
|
return "<html" + attrs + ' data-theme="' + t + '">';
|
||||||
|
});
|
||||||
|
const overlay =
|
||||||
|
'<div id="inst-nav-overlay" aria-hidden="true" style="position:fixed;inset:0;z-index:2147483646;background:' +
|
||||||
|
bg +
|
||||||
|
';opacity:1;pointer-events:auto"></div>';
|
||||||
|
if (/<body[^>]*>/i.test(out)) {
|
||||||
|
out = out.replace(/<body([^>]*)>/i, "<body$1>" + overlay);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中控 iframe:fetch 换页 + 页内遮罩,避免整页卸载与中控侧长时间空白。 */
|
||||||
function initHubEmbedInFrameNav() {
|
function initHubEmbedInFrameNav() {
|
||||||
if (!isHubLinked()) return;
|
if (!isHubLinked()) return;
|
||||||
|
|
||||||
|
let navToken = 0;
|
||||||
|
const prefetch = new Map();
|
||||||
|
const PREFETCH_MAX = 8;
|
||||||
|
|
||||||
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;
|
if (a.hasAttribute("download") || a.target === "_blank") return false;
|
||||||
return !!a.closest(".top-nav, .strategy-subnav");
|
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();
|
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(
|
document.addEventListener(
|
||||||
@@ -376,12 +489,30 @@
|
|||||||
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();
|
||||||
navigateTopNav(nextHref);
|
void navigateInFrame(nextHref);
|
||||||
},
|
},
|
||||||
true
|
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() {
|
function purgeLegacySoftNavCache() {
|
||||||
@@ -408,7 +539,6 @@
|
|||||||
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 (_) {}
|
||||||
@@ -438,7 +568,11 @@
|
|||||||
syncInlineStyles(get());
|
syncInlineStyles(get());
|
||||||
patchHubNavLinks(get());
|
patchHubNavLinks(get());
|
||||||
observeDynamicLists();
|
observeDynamicLists();
|
||||||
if (isHubLinked()) notifyParentFrameReady();
|
if (isHubLinked()) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => notifyParentFrameReady());
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", onReady);
|
document.addEventListener("DOMContentLoaded", onReady);
|
||||||
|
|||||||
Reference in New Issue
Block a user