/** * 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/. * 各 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", 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); }); } 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"); 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 || path.indexOf("/api/options/cross-transfer") >= 0) { setSettingsSubTabInUrl("options_transfer"); return "options_transfer"; } return ""; } 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 === "system_guide" || 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); 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);