Files
crypto_monitor/lib/common/static/instance_embed.js
T
dekun 27c116f872 Stop embed form interceptor from blanking trade records after journal save.
Exclude journal-form from InstanceEmbed submit reload, stopImmediatePropagation on AJAX save, and force-reload trades when the table is stuck on loading.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 15:28:52 +08:00

534 lines
16 KiB
JavaScript

/**
* 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/<tab>.
* 各 tab 面板常驻 DOM,切换时 show/hide;脚本延后到首次激活,回访零请求.
*/
(function (global) {
const TAB_PATH = {
dashboard: "/dashboard",
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();
/** 自带 AJAX/校验提交的表单,勿在捕获阶段再 fetch+reloadCurrentTab(会卡在「加载中…」) */
const CUSTOM_SUBMIT_FORM_IDS = new Set([
"add-order-form",
"key-form",
"roll-form",
"journal-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 (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.init === "function") {
global.InstanceDashboard.init(!!revisit);
}
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
global.initStrategyRollForm();
}
if (tab === "records") {
if (global.RecordsReviewPage && typeof global.RecordsReviewPage.init === "function") {
global.RecordsReviewPage.init({ refresh: !!revisit });
} else {
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);
}
if (global.JournalFormSave && typeof global.JournalFormSave.init === "function") {
global.JournalFormSave.init();
}
}
}
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);