4ade3be2eb
Co-authored-by: Cursor <cursoragent@cursor.com>
600 lines
19 KiB
JavaScript
600 lines
19 KiB
JavaScript
/**
|
|
* 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/<tab>.
|
|
* 各 tab 面板常驻 DOM,切换时 show/hide;脚本延后到首次激活,回访零请求.
|
|
*/
|
|
(function (global) {
|
|
const TAB_PATH = {
|
|
dashboard: "/dashboard",
|
|
account_ledger: "/account_ledger",
|
|
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",
|
|
system_guide: "/system_guide",
|
|
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);
|
|
});
|
|
if (global.InstanceMobileNav && typeof global.InstanceMobileNav.onTabChange === "function") {
|
|
global.InstanceMobileNav.onTabChange(tab);
|
|
} else if (global.InstanceMobileNav && typeof global.InstanceMobileNav.syncTabActive === "function") {
|
|
global.InstanceMobileNav.syncTabActive(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 (tab === "account_ledger" && global.AccountLedgerPage && typeof global.AccountLedgerPage.boot === "function") {
|
|
global.AccountLedgerPage.boot();
|
|
}
|
|
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");
|
|
if (tab === "settings") {
|
|
try {
|
|
const st = new URLSearchParams(location.search).get("settings_tab");
|
|
if (st) parts.push("settings_tab=" + encodeURIComponent(st));
|
|
} catch (_) {}
|
|
}
|
|
return url + "?" + parts.join("&");
|
|
}
|
|
|
|
function setSettingsSubTabInUrl(key) {
|
|
if (!key) return;
|
|
try {
|
|
const q = new URLSearchParams(location.search);
|
|
q.set("tab", "settings");
|
|
q.set("settings_tab", key);
|
|
q.set("embed", "1");
|
|
history.replaceState(null, "", "/embed?" + q.toString());
|
|
} catch (_) {}
|
|
}
|
|
|
|
function activateSettingsSubTab(key) {
|
|
if (!key) return;
|
|
setSettingsSubTabInUrl(key);
|
|
const pane = tabPanes.get("settings") || document;
|
|
const radio = pane.querySelector(
|
|
'input.env-tab-radio[data-settings-tab="' + key + '"]'
|
|
);
|
|
if (radio) radio.checked = true;
|
|
}
|
|
|
|
function formActionPath(form) {
|
|
try {
|
|
return new URL(form.action || "", location.href).pathname.replace(/\/$/, "") || "/";
|
|
} catch (_) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function maybeKeepSettingsSubTabAfterForm(form) {
|
|
const path = formActionPath(form);
|
|
if (path === "/manual_transfer") {
|
|
setSettingsSubTabInUrl("transfer");
|
|
return "transfer";
|
|
}
|
|
if (path.indexOf("/api/options/transfer") >= 0) {
|
|
setSettingsSubTabInUrl("options_transfer");
|
|
return "options_transfer";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
async function fetchTabHtml(tab) {
|
|
const r = await fetch(embedPageUrl(tab), {
|
|
credentials: "same-origin",
|
|
cache: "no-store",
|
|
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 === "system_guide" ||
|
|
tab === "env_config" ||
|
|
tab === "options_review";
|
|
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);
|
|
const keepSub = maybeKeepSettingsSubTabAfterForm(form);
|
|
return fetch(form.action, {
|
|
method: form.method || "POST",
|
|
body: fd,
|
|
credentials: "same-origin",
|
|
redirect: "manual",
|
|
})
|
|
.then(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)))
|
|
.catch(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)));
|
|
}
|
|
|
|
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);
|
|
const keepSub = maybeKeepSettingsSubTabAfterForm(form);
|
|
fetch(form.action, {
|
|
method: form.method || "POST",
|
|
body: fd,
|
|
credentials: "same-origin",
|
|
redirect: "manual",
|
|
})
|
|
.then(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)))
|
|
.catch(() => reloadCurrentTab().then(() => activateSettingsSubTab(keepSub)));
|
|
},
|
|
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);
|