/** * 中控 iframe 壳:顶栏/统计常驻,tab 内容走 /api/embed/page/。 * 各 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);