+
+{% if orders %}
+
+
+{% endif %}
+
+
+
diff --git a/hub_bridge.py b/hub_bridge.py
index 1c4cd62..722ff48 100644
--- a/hub_bridge.py
+++ b/hub_bridge.py
@@ -28,6 +28,46 @@ from hub_sso import (
)
+def _merge_query_into_path(path: str, **params: str) -> str:
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
+
+ split = urlsplit(path or "/")
+ q = list(parse_qsl(split.query, keep_blank_values=True))
+ keys = {k for k, _ in q}
+ for k, v in params.items():
+ if not v or k in keys:
+ continue
+ q.append((k, str(v)))
+ return urlunsplit((split.scheme, split.netloc, split.path, urlencode(q), split.fragment))
+
+
+def install_instance_theme_static(app) -> None:
+ """仓库根 static/instance_theme.* 供四所页面共用。"""
+ import os
+
+ from flask import Response, send_file
+
+ repo_static = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
+ assets = {
+ "instance_theme.js": "application/javascript; charset=utf-8",
+ "instance_theme.css": "text/css; charset=utf-8",
+ }
+
+ for name, mime in assets.items():
+ path = os.path.join(repo_static, name)
+
+ def _view(p=path, m=mime):
+ if not os.path.isfile(p):
+ return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
+ return send_file(p, mimetype=m)
+
+ app.add_url_rule(
+ f"/static/{name}",
+ endpoint=f"repo_static_{name.replace('.', '_')}",
+ view_func=_view,
+ )
+
+
def _hub_auth_required(f):
@wraps(f)
def wrapped(*args, **kwargs):
@@ -149,6 +189,7 @@ def install_on_app(
}
install_hub_embed_headers(app)
configure_hub_embed_session(app)
+ install_instance_theme_static(app)
register_hub_routes(app)
@@ -421,11 +462,22 @@ def register_hub_routes(app):
if _sso_wants_embed_auth() and request.is_secure:
boot = mint_hub_embed_bootstrap(ex, next_path)
if boot:
- q = urlencode({"t": boot, "next": next_path, "embed": "1"})
- return redirect(f"/hub-embed-auth?{q}")
+ from urllib.parse import urlencode as _ue
+
+ qdict = {"t": boot, "next": next_path, "embed": "1"}
+ ht0 = (request.args.get("hub_theme") or "").strip().lower()
+ if ht0 in ("light", "dark"):
+ qdict["hub_theme"] = ht0
+ return redirect(f"/hub-embed-auth?{_ue(qdict)}")
session["logged_in"] = True
session.modified = True
- return redirect(next_path)
+ dest = next_path
+ if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
+ dest = _merge_query_into_path(dest, embed="1")
+ ht = (request.args.get("hub_theme") or "").strip().lower()
+ if ht in ("light", "dark"):
+ dest = _merge_query_into_path(dest, hub_theme=ht)
+ return redirect(dest)
hint = err or "校验失败"
flash(
f"中控 SSO 未生效({hint})。"
@@ -449,7 +501,13 @@ def register_hub_routes(app):
if ok:
session["logged_in"] = True
session.modified = True
- return redirect(next_path)
+ dest = next_path
+ if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
+ dest = _merge_query_into_path(dest, embed="1")
+ ht = (request.args.get("hub_theme") or "").strip().lower()
+ if ht in ("light", "dark"):
+ dest = _merge_query_into_path(dest, hub_theme=ht)
+ return redirect(dest)
hint = err or "校验失败"
flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。")
return redirect("/login")
diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py
index 25a6681..a4a449e 100644
--- a/manual_trading_hub/hub.py
+++ b/manual_trading_hub/hub.py
@@ -1057,7 +1057,11 @@ def _require_hub_logged_in(request: Request) -> None:
@app.get("/api/instance/open-url")
def api_instance_open_url(
- request: Request, exchange_id: str, next: str = "/", embed: str = ""
+ request: Request,
+ exchange_id: str,
+ next: str = "/",
+ embed: str = "",
+ hub_theme: str = "",
):
"""已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。"""
_require_hub_logged_in(request)
@@ -1079,6 +1083,9 @@ def api_instance_open_url(
params = {"token": token, "next": nxt}
if (embed or "").strip().lower() in ("1", "true", "yes", "on"):
params["embed"] = "1"
+ ht = (hub_theme or "").strip().lower()
+ if ht in ("light", "dark"):
+ params["hub_theme"] = ht
q = urlencode(params)
return {
"ok": True,
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 5c809c6..0747c91 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -56,6 +56,9 @@
const next = nextPath || "/";
const q = new URLSearchParams({ exchange_id: String(exchangeId), next });
if (options.embed) q.set("embed", "1");
+ if (globalThis.HubTheme && typeof HubTheme.get === "function") {
+ q.set("hub_theme", HubTheme.get());
+ }
const r = await apiFetch("/api/instance/open-url?" + q.toString());
const j = await r.json();
if (!j.ok || !j.url) {
@@ -135,6 +138,16 @@
shell.classList.remove("hidden");
shell.setAttribute("aria-hidden", "false");
document.body.classList.add("hub-instance-frame-open");
+ frame.addEventListener("load", function syncInstanceFrameTheme() {
+ try {
+ if (globalThis.HubTheme && typeof HubTheme.get === "function" && frame.contentWindow) {
+ frame.contentWindow.postMessage(
+ { type: "hub-theme-sync", theme: HubTheme.get() },
+ "*"
+ );
+ }
+ } catch (_) {}
+ });
}
function closeInstanceFrame() {
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index a8a3da6..1ff8c36 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -2,7 +2,7 @@
-
+
@@ -248,7 +248,7 @@
-
-
+
+
diff --git a/manual_trading_hub/static/theme.js b/manual_trading_hub/static/theme.js
index d7df38b..15da7d0 100644
--- a/manual_trading_hub/static/theme.js
+++ b/manual_trading_hub/static/theme.js
@@ -15,6 +15,15 @@
}
}
+ function broadcastThemeToInstances() {
+ const msg = { type: "hub-theme-sync", theme: get() };
+ document.querySelectorAll("iframe#instance-frame, iframe.instance-frame").forEach((frame) => {
+ try {
+ if (frame.contentWindow) frame.contentWindow.postMessage(msg, "*");
+ } catch (_) {}
+ });
+ }
+
function apply(theme) {
const t = normalize(theme);
const root = document.documentElement;
@@ -26,6 +35,7 @@
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } }));
+ broadcastThemeToInstances();
return t;
}
diff --git a/scripts/patch_instance_theme_templates.py b/scripts/patch_instance_theme_templates.py
new file mode 100644
index 0000000..dc9a4da
--- /dev/null
+++ b/scripts/patch_instance_theme_templates.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+"""为四所 templates 注入 instance_theme 脚本/样式与切换按钮。"""
+from __future__ import annotations
+
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+EXCHANGES = ("crypto_monitor_binance", "crypto_monitor_okx", "crypto_monitor_gate", "crypto_monitor_gate_bot")
+FILES = ("index.html", "login.html", "key_focus_v2.html", "order_focus_v2.html")
+
+SCRIPT_TAG = ' \n'
+CSS_LINK = ' \n'
+
+THEME_TOGGLE = """
+
+
+
+"""
+
+INDEX_HEADER_OLD = """
+
加密货币|交易监控 + AI复盘一体化
+
{{ exchange_display }}
+
"""
+
+INDEX_HEADER_NEW = """
+
加密货币|交易监控 + AI复盘一体化
+
+
{{ exchange_display }}
+""" + THEME_TOGGLE + """
+
"""
+
+
+def patch_file(path: Path) -> bool:
+ text = path.read_text(encoding="utf-8")
+ orig = text
+ if 'data-theme="dark"' not in text:
+ text = text.replace('', '', 1)
+ if "/static/instance_theme.js" not in text:
+ text = text.replace(
+ "",
+ "\n" + SCRIPT_TAG.strip() + "\n",
+ 1,
+ )
+ if "/static/instance_theme.css" not in text:
+ text = text.replace("", "\n" + CSS_LINK, 1)
+ if path.name == "index.html" and INDEX_HEADER_OLD in text and "instance-theme-toggle" not in text:
+ text = text.replace(INDEX_HEADER_OLD, INDEX_HEADER_NEW)
+ if path.name == "login.html" and "instance-theme-toggle" not in text:
+ text = text.replace(
+ "",
+ '
\n' + THEME_TOGGLE + "
\n",
+ 1,
+ )
+ if path.name == "key_focus_v2.html" and "instance-theme-toggle" not in text:
+ marker = '
'
+ if marker in text:
+ text = text.replace(
+ marker,
+ marker + "\n " + THEME_TOGGLE.replace("\n", "\n "),
+ 1,
+ )
+ if path.name == "order_focus_v2.html" and "instance-theme-toggle" not in text:
+ marker = '