Add hub iframe embed shell with tab fragment API.
Replace full-page soft nav with a persistent shell and /api/embed/page loads so tab switches in the hub iframe avoid document.write flicker. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/<tab>。
|
||||
*/
|
||||
(function (global) {
|
||||
const TAB_PATH = {
|
||||
key_monitor: "/key_monitor",
|
||||
trade: "/trade",
|
||||
strategy: "/strategy",
|
||||
strategy_records: "/strategy/records",
|
||||
records: "/records",
|
||||
stats: "/stats",
|
||||
};
|
||||
|
||||
let navToken = 0;
|
||||
let loadingTab = false;
|
||||
|
||||
function isEmbedShell() {
|
||||
return document.body && document.body.getAttribute("data-embed-shell") === "1";
|
||||
}
|
||||
|
||||
function getTab() {
|
||||
try {
|
||||
const t = new URLSearchParams(location.search).get("tab");
|
||||
if (t) return t;
|
||||
} catch (_) {}
|
||||
return document.body.getAttribute("data-page") || "trade";
|
||||
}
|
||||
|
||||
function listWindowQueryString() {
|
||||
if (typeof global.listWindowQueryString === "function") {
|
||||
return global.listWindowQueryString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function notifyParentNavStart() {
|
||||
try {
|
||||
window.parent.postMessage({ type: "instance-frame-navigating" }, "*");
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function notifyParentReady() {
|
||||
try {
|
||||
window.parent.postMessage({ type: "instance-frame-ready" }, "*");
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function setNavActive(tab) {
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => {
|
||||
a.classList.toggle("active", a.getAttribute("data-embed-tab") === tab);
|
||||
});
|
||||
}
|
||||
|
||||
function syncUrl(tab, replace) {
|
||||
const q = new URLSearchParams(location.search);
|
||||
q.set("tab", tab);
|
||||
q.set("embed", "1");
|
||||
const qs = q.toString();
|
||||
const url = "/embed?" + qs;
|
||||
if (replace) history.replaceState({ embedTab: tab }, "", url);
|
||||
else history.pushState({ embedTab: tab }, "", url);
|
||||
}
|
||||
|
||||
function runPageInit(tab) {
|
||||
document.body.setAttribute("data-page", tab);
|
||||
if (typeof global.attachListWindowToExports === "function") {
|
||||
global.attachListWindowToExports();
|
||||
}
|
||||
if (tab === "trade") {
|
||||
if (typeof global.refreshOrderDefaults === "function") global.refreshOrderDefaults();
|
||||
}
|
||||
if (tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
|
||||
global.KeyMonitorForm.init();
|
||||
}
|
||||
if (tab === "records") {
|
||||
if (typeof global.loadJournals === "function") global.loadJournals();
|
||||
if (typeof global.loadReviews === "function") global.loadReviews();
|
||||
if (typeof global.toggleReviewMode === "function") global.toggleReviewMode();
|
||||
}
|
||||
if (typeof global.refreshPriceSnapshotConditional === "function") {
|
||||
global.refreshPriceSnapshotConditional();
|
||||
}
|
||||
}
|
||||
|
||||
function injectFragment(html) {
|
||||
const root = document.getElementById("embed-page-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = html;
|
||||
root.querySelectorAll("script").forEach((old) => {
|
||||
const s = document.createElement("script");
|
||||
if (old.src) s.src = old.src;
|
||||
else s.textContent = old.textContent;
|
||||
old.replaceWith(s);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTab(tab, opts) {
|
||||
const options = opts || {};
|
||||
if (!tab || loadingTab) return;
|
||||
const token = ++navToken;
|
||||
loadingTab = true;
|
||||
notifyParentNavStart();
|
||||
try {
|
||||
const qs = listWindowQueryString();
|
||||
const url = "/api/embed/page/" + encodeURIComponent(tab) + (qs ? "?" + qs : "");
|
||||
const r = await fetch(url, { credentials: "same-origin" });
|
||||
if (token !== navToken) return;
|
||||
const j = await r.json();
|
||||
if (!j.ok || !j.html) throw new Error(j.msg || "加载失败");
|
||||
injectFragment(j.html);
|
||||
setNavActive(tab);
|
||||
if (!options.skipUrl) syncUrl(tab, !!options.replace);
|
||||
runPageInit(tab);
|
||||
} catch (e) {
|
||||
if (token === navToken) {
|
||||
const flash = document.getElementById("embed-flash");
|
||||
if (flash) {
|
||||
flash.style.display = "";
|
||||
flash.textContent = String(e && e.message ? e.message : e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (token === navToken) {
|
||||
loadingTab = false;
|
||||
notifyParentReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reloadCurrentTab() {
|
||||
return loadTab(getTab(), { replace: true, skipUrl: true });
|
||||
}
|
||||
|
||||
function patchApplyListWindow() {
|
||||
if (typeof global.applyListWindow !== "function") return;
|
||||
global.applyListWindow = function embedApplyListWindow() {
|
||||
void loadTab(getTab(), { replace: true });
|
||||
};
|
||||
}
|
||||
|
||||
function patchHardNavigations() {
|
||||
const resubmitPaths =
|
||||
/^\/(del_|delete_|add_|stop_|strategy\/|trend_|roll_|cancel_|place_)/;
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(ev) => {
|
||||
if (!isEmbedShell()) return;
|
||||
const a = ev.target.closest("a[href]");
|
||||
if (!a || ev.defaultPrevented) return;
|
||||
if (a.closest(".embed-top-nav")) return;
|
||||
if (a.hasAttribute("download") || a.target === "_blank") return;
|
||||
const raw = a.getAttribute("href");
|
||||
if (!raw || raw.startsWith("#") || raw.startsWith("javascript:")) return;
|
||||
let url;
|
||||
try {
|
||||
url = new URL(raw, location.href);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (url.origin !== location.origin) return;
|
||||
if (url.pathname.startsWith("/export/") || url.pathname.startsWith("/order_focus") || url.pathname.startsWith("/key_focus")) {
|
||||
return;
|
||||
}
|
||||
if (!resubmitPaths.test(url.pathname)) return;
|
||||
ev.preventDefault();
|
||||
fetch(url.pathname + url.search, { credentials: "same-origin", redirect: "manual" })
|
||||
.then(() => reloadCurrentTab())
|
||||
.catch(() => reloadCurrentTab());
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"submit",
|
||||
(ev) => {
|
||||
if (!isEmbedShell()) return;
|
||||
const form = ev.target;
|
||||
if (!(form instanceof HTMLFormElement)) return;
|
||||
if (form.method && form.method.toUpperCase() === "GET") return;
|
||||
ev.preventDefault();
|
||||
const fd = new FormData(form);
|
||||
fetch(form.action, {
|
||||
method: form.method || "POST",
|
||||
body: fd,
|
||||
credentials: "same-origin",
|
||||
redirect: "manual",
|
||||
})
|
||||
.then(() => reloadCurrentTab())
|
||||
.catch(() => reloadCurrentTab());
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function bindNav() {
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => {
|
||||
a.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
const tab = a.getAttribute("data-embed-tab");
|
||||
if (!tab || tab === getTab()) return;
|
||||
void loadTab(tab);
|
||||
});
|
||||
});
|
||||
window.addEventListener("popstate", () => {
|
||||
const tab = getTab();
|
||||
void loadTab(tab, { replace: true, skipUrl: true });
|
||||
});
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (!isEmbedShell()) return;
|
||||
patchApplyListWindow();
|
||||
patchHardNavigations();
|
||||
bindNav();
|
||||
runPageInit(getTab());
|
||||
notifyParentReady();
|
||||
}
|
||||
|
||||
global.InstanceEmbed = {
|
||||
loadTab,
|
||||
reloadCurrentTab,
|
||||
getTab,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
Reference in New Issue
Block a user