diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 1194d47..18e2517 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -290,7 +290,7 @@ def resolve_path(path_value): app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", "crypto_monitor_2026_secret_key") -from lib.instance.instance_embed_lib import attach_embed_templates +from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled attach_embed_templates(app, _REPO_ROOT) @@ -6510,7 +6510,11 @@ def render_main_page(page="trade", embed_mode=None): options_trading_usdc = None options_funding_usdc = None options_funding_usdt = None - if OKX_OPTIONS_ENABLED and exchange_options.apiKey: + if ( + OKX_OPTIONS_ENABLED + and exchange_options.apiKey + and embed_mode != "fragment" + ): try: from lib.exchange.okx_options_lib import ( fetch_options_funding_usdc, @@ -6601,22 +6605,29 @@ def render_main_page(page="trade", embed_mode=None): hard_limit=DAILY_OPEN_HARD_LIMIT, extra_blocks=not risk_status.get("can_trade", True), ) - key_rule_ctx = key_monitor_rule_template_context( - kline_timeframe=KLINE_TIMEFRAME, - key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT, - key_volume_ma_bars=KEY_VOLUME_MA_BARS, - key_volume_ratio_min=KEY_VOLUME_RATIO_MIN, - key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR, - key_daily_volume_rank_max=KEY_DAILY_VOLUME_RANK_MAX, - key_confirm_breakout_bar=KEY_CONFIRM_BREAKOUT_BAR, - key_confirm_bar=KEY_CONFIRM_BAR, - key_alert_max_times=KEY_ALERT_MAX_TIMES, - key_alert_interval_minutes=KEY_ALERT_INTERVAL_MINUTES, - key_stop_outside_breakout_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT, - key_trend_stop_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT, - false_breakout_validity_hours=FALSE_BREAKOUT_VALIDITY_HOURS, - trigger_entry_validity_hours=TRIGGER_ENTRY_VALIDITY_HOURS, - ) + key_rule_ctx = {} + if page in ("key_monitor", "trade") or page in ( + "strategy", + "strategy_trend", + "strategy_roll", + "strategy_records", + ): + key_rule_ctx = key_monitor_rule_template_context( + kline_timeframe=KLINE_TIMEFRAME, + key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT, + key_volume_ma_bars=KEY_VOLUME_MA_BARS, + key_volume_ratio_min=KEY_VOLUME_RATIO_MIN, + key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR, + key_daily_volume_rank_max=KEY_DAILY_VOLUME_RANK_MAX, + key_confirm_breakout_bar=KEY_CONFIRM_BREAKOUT_BAR, + key_confirm_bar=KEY_CONFIRM_BAR, + key_alert_max_times=KEY_ALERT_MAX_TIMES, + key_alert_interval_minutes=KEY_ALERT_INTERVAL_MINUTES, + key_stop_outside_breakout_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT, + key_trend_stop_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT, + false_breakout_validity_hours=FALSE_BREAKOUT_VALIDITY_HOURS, + trigger_entry_validity_hours=TRIGGER_ENTRY_VALIDITY_HOURS, + ) strategy_extra = {} if plan.strategy: from lib.strategy.strategy_ui import strategy_render_extras @@ -6768,36 +6779,54 @@ def index(): @app.route("/key_monitor") @login_required def key_monitor_page(): + redir = redirect_to_embed_shell_if_enabled("key_monitor") + if redir is not None: + return redir return render_main_page("key_monitor") @app.route("/trade") @login_required def trade_page(): + redir = redirect_to_embed_shell_if_enabled("trade") + if redir is not None: + return redir return render_main_page("trade") @app.route("/records") @login_required def records_page(): + redir = redirect_to_embed_shell_if_enabled("records") + if redir is not None: + return redir return render_main_page("records") @app.route("/stats") @login_required def stats_page(): + redir = redirect_to_embed_shell_if_enabled("stats") + if redir is not None: + return redir return render_main_page("stats") @app.route("/settings") @login_required def settings_page(): + redir = redirect_to_embed_shell_if_enabled("settings") + if redir is not None: + return redir return render_main_page("settings") @app.route("/options") @login_required def options_main_page(): + redir = redirect_to_embed_shell_if_enabled("options") + if redir is not None: + return redir return render_main_page("options") diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index b3deb0a..1e7aafd 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -7,6 +7,7 @@ trade: "/trade", strategy: "/strategy", strategy_records: "/strategy/records", + options: "/options", records: "/records", stats: "/stats", settings: "/settings", @@ -14,6 +15,7 @@ let navToken = 0; let loadingTab = false; + const tabHtmlCache = new Map(); /** 自带校验后 form.submit() 的表单,勿在捕获阶段再 fetch 一份(会双发 POST) */ const CUSTOM_SUBMIT_FORM_IDS = new Set(["add-order-form", "key-form", "roll-form"]); @@ -112,20 +114,61 @@ }); } + 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 || tabHtmlCache.has(tab)) return; + fetchTabHtml(tab) + .then((html) => { + tabHtmlCache.set(tab, html); + }) + .catch(() => {}); + } + + function clearTabCache() { + tabHtmlCache.clear(); + } + async function loadTab(tab, opts) { const options = opts || {}; if (!tab || loadingTab) return; const token = ++navToken; + const force = !!options.force; + const cached = !force && tabHtmlCache.get(tab); + + if (cached) { + injectFragment(cached); + setNavActive(tab); + if (!options.skipUrl) syncUrl(tab, !!options.replace); + runPageInit(tab); + warmTabCache(tab); + return; + } + loadingTab = true; setRootLoading(true); try { - const qs = listWindowQueryString(); - const url = "/api/embed/page/" + encodeURIComponent(tab) + (qs ? "?" + qs : ""); - const r = await fetch(url, { credentials: "same-origin" }); + const html = await fetchTabHtml(tab); if (token !== navToken) return; - const j = await r.json(); - if (!j.ok || !j.html) throw new Error(j.msg || "加载失败"); - injectFragment(j.html); + tabHtmlCache.set(tab, html); + injectFragment(html); setNavActive(tab); if (!options.skipUrl) syncUrl(tab, !!options.replace); runPageInit(tab); @@ -146,7 +189,8 @@ } function reloadCurrentTab() { - return loadTab(getTab(), { replace: true, skipUrl: true }); + tabHtmlCache.delete(getTab()); + return loadTab(getTab(), { replace: true, skipUrl: true, force: true }); } function postFormAndReload(form, label) { @@ -172,6 +216,7 @@ function patchApplyListWindow() { if (typeof global.applyListWindow !== "function") return; global.applyListWindow = function embedApplyListWindow() { + clearTabCache(); const qs = listWindowQueryString(); const tab = getTab(); const q = new URLSearchParams(qs); @@ -239,6 +284,9 @@ 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"); @@ -268,6 +316,7 @@ reloadCurrentTab, getTab, postFormAndReload, + clearTabCache, }; if (document.readyState === "loading") { diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index ce781cf..f99644a 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -4,10 +4,12 @@ const root = document.getElementById("options-root"); if (!root) return; + const panelCache = (window.__optionsPanelCache = window.__optionsPanelCache || {}); + const state = { underlying: root.dataset.defaultUnderly || "ETH", optType: "C", - chain: null, + chain: panelCache.chain || null, selectedInst: null, }; @@ -254,6 +256,9 @@ return; } state.chain = d; + panelCache.chain = d; + panelCache.underlying = state.underlying; + panelCache.optType = state.optType; state.selectedInst = null; parkOrderPanel(); renderExpiries(); @@ -457,6 +462,28 @@ refreshHistory(); } + function bootOptionsPanel() { + updateSizeInputs(); + const hasCache = + panelCache.chain && + panelCache.underlying === state.underlying && + panelCache.optType === state.optType; + if (hasCache) { + state.chain = panelCache.chain; + renderExpiries(); + renderStrikes(); + refreshAllPositions(); + requestAnimationFrame(function () { + loadChain(); + }); + return; + } + requestAnimationFrame(function () { + loadChain(); + refreshAllPositions(); + }); + } + document.querySelectorAll(".opt-uly-btn").forEach(function (btn) { btn.addEventListener("click", function () { document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); }); @@ -495,7 +522,5 @@ }); }); - updateSizeInputs(); - loadChain(); - refreshAllPositions(); + bootOptionsPanel(); })(); diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py index ad4bc11..5d57bfe 100644 --- a/lib/instance/instance_embed_lib.py +++ b/lib/instance/instance_embed_lib.py @@ -72,6 +72,22 @@ def embed_shell_enabled() -> bool: return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on") +def redirect_to_embed_shell_if_enabled(page: str): + """直连 /trade 等整页路由时,重定向到 embed 壳(顶栏常驻、tab 软切换)。""" + from flask import redirect, request, urlencode + + if not embed_shell_enabled(): + return None + if (request.args.get("embed") or "").strip() == "1": + return None + if (request.path or "").rstrip("/") == "/embed": + return None + q = {k: v for k, v in request.args.items()} + q["tab"] = page + q["embed"] = "1" + return redirect("/embed?" + urlencode(q)) + + def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str: """embed=1 打开时:/trade → /embed?tab=trade&embed=1""" if not embed_shell_enabled(): diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 5327a69..1aa7ef1 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -90,6 +90,6 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj {% include 'embed_boot_scripts.html' %} - + diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 6370848..649ba4a 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -121,4 +121,4 @@ - +