Files
crypto_monitor/lib/common/static/instance_embed.js
T
dekun 10128d18bc Add OKX options review module with hedge plan entries.
Import closed OKX option history and closed hedge plans into one list for journaling, images, and stats without mixing contract reviews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 09:35:19 +08:00

518 lines
16 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",
options_review: "/options/review",
hedge_plan: "/hedge-plan",
records: "/records",
stats: "/stats",
risk_policy: "/risk_policy",
env_config: "/env_config",
settings: "/settings",
};
let navToken = 0;
let loadingTab = false;
let pendingTabLoad = null;
const tabPanes = new Map();
const tabBooted = new Set();
/** 自带校验后 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 pageNavAllowed(tab) {
if (global.InstanceSettingsPrefs && typeof global.InstanceSettingsPrefs.pageNavAllowed === "function") {
return global.InstanceSettingsPrefs.pageNavAllowed(tab);
}
return true;
}
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 notifyParentTabSwitch(tab) {
try {
window.parent.postMessage({ type: "instance-frame-navigating", embedShellTab: true, tab: tab }, "*");
} catch (_) {}
}
function runPageInit(tab, opts) {
const options = opts || {};
const revisit = !!options.revisit;
document.body.setAttribute("data-page", tab);
if (!revisit && typeof global.attachListWindowToExports === "function") {
global.attachListWindowToExports();
}
if (tab === "trade") {
if (!revisit && typeof global.refreshOrderDefaults === "function") global.refreshOrderDefaults();
if (!revisit && typeof global.initOrderEntryModelSelect === "function") {
const root = pageRoot() || document;
global.initOrderEntryModelSelect(root);
}
if (!revisit && global.ManualOrderRrPreview && typeof global.ManualOrderRrPreview.wire === "function") {
global.ManualOrderRrPreview.wire();
}
}
if (!revisit && tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
global.KeyMonitorForm.init();
}
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
global.initStrategyRollForm();
}
if (tab === "records") {
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
if (global.InstanceTheme && typeof global.InstanceTheme.initReviewEditModeSync === "function") {
global.InstanceTheme.initReviewEditModeSync();
} else if (typeof global.toggleReviewMode === "function") {
global.toggleReviewMode();
}
}
if (tab === "stats") {
if (typeof global.initStatsSegmentFromUrl === "function") global.initStatsSegmentFromUrl();
}
if (tab === "settings" || tab === "env_config") {
if (global.InstanceSettingsPrefs) {
if (typeof global.InstanceSettingsPrefs.bindEvents === "function") {
global.InstanceSettingsPrefs.bindEvents();
}
if (tab === "settings" && typeof global.InstanceSettingsPrefs.loadDisplayPrefsForm === "function") {
global.InstanceSettingsPrefs.loadDisplayPrefsForm();
}
if (tab === "env_config") {
if (typeof global.InstanceSettingsPrefs.loadEnvConfig === "function") {
global.InstanceSettingsPrefs.loadEnvConfig();
}
if (typeof global.InstanceSettingsPrefs.bindEnvTabs === "function") {
global.InstanceSettingsPrefs.bindEnvTabs();
}
}
}
}
if (!revisit) {
if (typeof global.refreshAccountSnapshot === "function") {
global.refreshAccountSnapshot({ silent: true });
}
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) => {
const on = name === tab;
pane.hidden = !on;
pane.classList.toggle("is-active-pane", on);
});
}
function bootPaneScripts(tab) {
if (tabBooted.has(tab)) return;
const pane = tabPanes.get(tab);
if (!pane) return;
runScripts(pane);
tabBooted.add(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;
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 is-active-pane";
pane.setAttribute("data-embed-pane", tab);
Array.from(root.childNodes).forEach((node) => pane.appendChild(node));
root.appendChild(pane);
tabPanes.set(tab, pane);
tabBooted.add(tab);
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 ct = (r.headers.get("content-type") || "").toLowerCase();
if (!ct.includes("application/json")) {
throw new Error("加载失败(HTTP " + r.status + ")");
}
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 preloadAllTabs() {
const tabs = Object.keys(TAB_PATH);
const current = getTab();
const heavyLast = new Set(["options", "records", "stats"]);
const ordered = tabs.filter((t) => t !== current && !heavyLast.has(t))
.concat(tabs.filter((t) => heavyLast.has(t) && t !== current));
let idx = 0;
function step() {
if (idx >= ordered.length) return;
const tab = ordered[idx++];
if (tabPanes.has(tab)) {
step();
return;
}
fetchTabHtml(tab)
.then((html) => {
if (!tabPanes.has(tab)) mountPane(tab, html);
})
.catch(() => {})
.finally(() => {
setTimeout(step, heavyLast.has(tab) ? 400 : 180);
});
}
const ric = global.requestIdleCallback || function (fn) {
setTimeout(fn, 2000);
};
ric(step);
}
function clearTabCache() {
tabPanes.forEach((pane) => pane.remove());
tabPanes.clear();
tabBooted.clear();
}
function syncShellChrome(tab) {
const hideTopBar = tab === "settings" || tab === "risk_policy" || tab === "env_config";
document.querySelectorAll(".instance-top-bar").forEach((el) => {
el.hidden = hideTopBar;
});
}
function initPaneThemeToggle(tab) {
if (tab !== "settings") return;
const pane = tabPanes.get(tab);
if (!pane || !global.InstanceTheme) return;
if (typeof global.InstanceTheme.initToggleUI === "function") {
global.InstanceTheme.initToggleUI(pane);
}
if (typeof global.InstanceTheme.syncToggleUI === "function") {
global.InstanceTheme.syncToggleUI(pane);
}
}
function activateTab(tab, opts) {
const options = opts || {};
const revisit = !!options.revisit;
const firstBoot = !tabBooted.has(tab);
syncShellChrome(tab);
showPane(tab);
setNavActive(tab);
if (!options.skipUrl) syncUrl(tab, !!options.replace);
notifyParentTabSwitch(tab);
if (firstBoot) {
bootPaneScripts(tab);
initPaneThemeToggle(tab);
runPageInit(tab, { revisit: false });
return;
}
if (revisit) {
document.body.setAttribute("data-page", tab);
runPageInit(tab, { revisit: true });
return;
}
runPageInit(tab, { revisit: false });
}
async function loadTab(tab, opts) {
const options = opts || {};
if (!tab) return;
if (!pageNavAllowed(tab)) {
void loadTab("trade", { replace: true });
return;
}
if (tabPanes.has(tab) && !options.force) {
activateTab(tab, Object.assign({}, options, { revisit: true }));
return;
}
if (loadingTab) {
pendingTabLoad = { tab: tab, opts: options };
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;
if (pendingTabLoad) {
const pending = pendingTabLoad;
pendingTabLoad = null;
if (pending.tab !== tab) void loadTab(pending.tab, pending.opts);
}
}
}
function reloadCurrentTab() {
const tab = getTab();
const pane = tabPanes.get(tab);
if (pane) pane.remove();
tabPanes.delete(tab);
tabBooted.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();
const bootTab = getTab();
if (!pageNavAllowed(bootTab)) {
void loadTab("trade", { replace: true });
return;
}
if (bootTab === "settings") {
initPaneThemeToggle("settings");
}
bindNav();
syncShellChrome(getTab());
runPageInit(getTab());
preloadAllTabs();
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);