444088baf3
Co-authored-by: Cursor <cursoragent@cursor.com>
369 lines
11 KiB
JavaScript
369 lines
11 KiB
JavaScript
/**
|
||
* 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/<tab>。
|
||
* 各 tab 面板常驻 DOM,切换时 show/hide,避免整页替换闪屏。
|
||
*/
|
||
(function (global) {
|
||
const TAB_PATH = {
|
||
key_monitor: "/key_monitor",
|
||
trade: "/trade",
|
||
strategy: "/strategy",
|
||
strategy_records: "/strategy/records",
|
||
options: "/options",
|
||
records: "/records",
|
||
stats: "/stats",
|
||
settings: "/settings",
|
||
};
|
||
|
||
let navToken = 0;
|
||
let loadingTab = false;
|
||
const tabPanes = new Map();
|
||
|
||
/** 自带校验后 form.submit() 的表单,勿在捕获阶段再 fetch 一份(会双发 POST) */
|
||
const CUSTOM_SUBMIT_FORM_IDS = new Set(["add-order-form", "key-form", "roll-form"]);
|
||
|
||
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 pageRoot() {
|
||
return document.getElementById("embed-page-root");
|
||
}
|
||
|
||
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 (typeof global.initOrderEntryModelSelect === "function") {
|
||
const root = pageRoot() || document;
|
||
global.initOrderEntryModelSelect(root);
|
||
}
|
||
if (global.ManualOrderRrPreview && typeof global.ManualOrderRrPreview.wire === "function") {
|
||
global.ManualOrderRrPreview.wire();
|
||
}
|
||
}
|
||
if (tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
|
||
global.KeyMonitorForm.init();
|
||
}
|
||
if (tab === "strategy" && typeof global.initStrategyRollForm === "function") {
|
||
global.initStrategyRollForm();
|
||
}
|
||
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 (tab === "stats") {
|
||
if (typeof global.initStatsSegmentFromUrl === "function") global.initStatsSegmentFromUrl();
|
||
}
|
||
if (typeof global.refreshPriceSnapshotConditional === "function") {
|
||
global.refreshPriceSnapshotConditional();
|
||
}
|
||
if (global.SymbolLivePrice && typeof global.SymbolLivePrice.init === "function") {
|
||
const root = pageRoot() || document;
|
||
global.SymbolLivePrice.init(root);
|
||
}
|
||
if (global.JournalUploadSlots && typeof global.JournalUploadSlots.init === "function") {
|
||
const root = pageRoot() || document;
|
||
global.JournalUploadSlots.init(root);
|
||
}
|
||
}
|
||
|
||
function runScripts(container) {
|
||
container.querySelectorAll("script").forEach((old) => {
|
||
const s = document.createElement("script");
|
||
if (old.src) s.src = old.src;
|
||
else s.textContent = old.textContent;
|
||
old.replaceWith(s);
|
||
});
|
||
}
|
||
|
||
function showPane(tab) {
|
||
tabPanes.forEach((pane, name) => {
|
||
pane.hidden = name !== tab;
|
||
});
|
||
}
|
||
|
||
function mountPane(tab, html) {
|
||
const root = pageRoot();
|
||
if (!root) return null;
|
||
const existing = tabPanes.get(tab);
|
||
if (existing) existing.remove();
|
||
|
||
const pane = document.createElement("div");
|
||
pane.className = "embed-tab-pane";
|
||
pane.setAttribute("data-embed-pane", tab);
|
||
pane.hidden = true;
|
||
|
||
const holder = document.createElement("div");
|
||
holder.innerHTML = html;
|
||
runScripts(holder);
|
||
while (holder.firstChild) pane.appendChild(holder.firstChild);
|
||
|
||
root.appendChild(pane);
|
||
tabPanes.set(tab, pane);
|
||
return pane;
|
||
}
|
||
|
||
function initBootPane() {
|
||
const root = pageRoot();
|
||
if (!root || tabPanes.size > 0) return;
|
||
const tab = getTab();
|
||
if (root.querySelector("[data-embed-pane]")) return;
|
||
if (!root.childNodes.length) return;
|
||
|
||
const pane = document.createElement("div");
|
||
pane.className = "embed-tab-pane";
|
||
pane.setAttribute("data-embed-pane", tab);
|
||
Array.from(root.childNodes).forEach((node) => pane.appendChild(node));
|
||
root.appendChild(pane);
|
||
tabPanes.set(tab, pane);
|
||
showPane(tab);
|
||
}
|
||
|
||
function embedPageUrl(tab) {
|
||
const qs = listWindowQueryString();
|
||
let url = "/api/embed/page/" + encodeURIComponent(tab);
|
||
const parts = [];
|
||
if (qs) parts.push(qs);
|
||
parts.push("embed=1");
|
||
return url + "?" + parts.join("&");
|
||
}
|
||
|
||
async function fetchTabHtml(tab) {
|
||
const r = await fetch(embedPageUrl(tab), {
|
||
credentials: "same-origin",
|
||
headers: { "X-Instance-Soft-Nav": "1" },
|
||
});
|
||
const j = await r.json();
|
||
if (!j.ok || !j.html) throw new Error(j.msg || "加载失败");
|
||
return j.html;
|
||
}
|
||
|
||
function warmTabCache(tab) {
|
||
if (!tab || tabPanes.has(tab) || loadingTab) return;
|
||
fetchTabHtml(tab)
|
||
.then((html) => {
|
||
if (!tabPanes.has(tab)) mountPane(tab, html);
|
||
})
|
||
.catch(() => {});
|
||
}
|
||
|
||
function clearTabCache() {
|
||
tabPanes.forEach((pane) => pane.remove());
|
||
tabPanes.clear();
|
||
}
|
||
|
||
function activateTab(tab, opts) {
|
||
const options = opts || {};
|
||
showPane(tab);
|
||
setNavActive(tab);
|
||
if (!options.skipUrl) syncUrl(tab, !!options.replace);
|
||
runPageInit(tab);
|
||
}
|
||
|
||
async function loadTab(tab, opts) {
|
||
const options = opts || {};
|
||
if (!tab) return;
|
||
|
||
if (tabPanes.has(tab) && !options.force) {
|
||
activateTab(tab, options);
|
||
return;
|
||
}
|
||
|
||
if (loadingTab) return;
|
||
const token = ++navToken;
|
||
loadingTab = true;
|
||
try {
|
||
const html = await fetchTabHtml(tab);
|
||
if (token !== navToken) return;
|
||
mountPane(tab, html);
|
||
activateTab(tab, options);
|
||
} 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;
|
||
}
|
||
}
|
||
|
||
function reloadCurrentTab() {
|
||
const tab = getTab();
|
||
const pane = tabPanes.get(tab);
|
||
if (pane) pane.remove();
|
||
tabPanes.delete(tab);
|
||
return loadTab(tab, { replace: true, skipUrl: true, force: true });
|
||
}
|
||
|
||
function postFormAndReload(form, label) {
|
||
if (!form) return Promise.resolve();
|
||
if (global.FormSubmitGuard) {
|
||
if (global.FormSubmitGuard.isLocked(form)) {
|
||
global.FormSubmitGuard.setSubmitLabel(form, label || "提交中…");
|
||
} else {
|
||
global.FormSubmitGuard.lock(form, label || "提交中…");
|
||
}
|
||
}
|
||
const fd = new FormData(form);
|
||
return fetch(form.action, {
|
||
method: form.method || "POST",
|
||
body: fd,
|
||
credentials: "same-origin",
|
||
redirect: "manual",
|
||
})
|
||
.then(() => reloadCurrentTab())
|
||
.catch(() => reloadCurrentTab());
|
||
}
|
||
|
||
function patchApplyListWindow() {
|
||
if (typeof global.applyListWindow !== "function") return;
|
||
global.applyListWindow = function embedApplyListWindow() {
|
||
clearTabCache();
|
||
const qs = listWindowQueryString();
|
||
const tab = getTab();
|
||
const q = new URLSearchParams(qs);
|
||
q.set("tab", tab);
|
||
q.set("embed", "1");
|
||
window.location.href = "/embed?" + q.toString();
|
||
};
|
||
}
|
||
|
||
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;
|
||
if (CUSTOM_SUBMIT_FORM_IDS.has(form.id)) 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("mouseenter", () => {
|
||
warmTabCache(a.getAttribute("data-embed-tab"));
|
||
});
|
||
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();
|
||
initBootPane();
|
||
bindNav();
|
||
runPageInit(getTab());
|
||
try {
|
||
window.parent.postMessage({ type: "instance-frame-ready" }, "*");
|
||
} catch (_) {}
|
||
}
|
||
|
||
global.InstanceEmbed = {
|
||
loadTab,
|
||
reloadCurrentTab,
|
||
getTab,
|
||
postFormAndReload,
|
||
clearTabCache,
|
||
};
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", boot);
|
||
} else {
|
||
boot();
|
||
}
|
||
})(typeof window !== "undefined" ? window : globalThis);
|