diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index e2dac27..abd449f 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -1,7 +1,9 @@ - + + + @@ -229,6 +231,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} + + {% macro period_stats(title, s) %} @@ -253,7 +257,21 @@

加密货币|交易监控 + AI复盘一体化

-
{{ exchange_display }}
+
+
{{ exchange_display }}
+
+ + +
+
关键位监控 diff --git a/crypto_monitor_binance/templates/key_focus_v2.html b/crypto_monitor_binance/templates/key_focus_v2.html index 26af46d..9caa241 100644 --- a/crypto_monitor_binance/templates/key_focus_v2.html +++ b/crypto_monitor_binance/templates/key_focus_v2.html @@ -1,261 +1,278 @@ - - - - - {{ exchange_display }} | 关键位放大 - - - -
-
-
-
- 返回首页 - 关键位放大(可输入币种){{ exchange_display }} -
-
最近刷新:--
-
- -
- - - - - - - - - - - - - - -
-
- -
-
-
交易对
-
-
监控类型
-
-
方向
-
-
上沿/阻力
-
-
下沿/支撑
-
-
现价
-
-
距上沿
-
-
距下沿
-
-
-
- -
-
- - - - + + + + + + + {{ exchange_display }} | 关键位放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 关键位放大(可输入币种){{ exchange_display }} +
+
最近刷新:--
+
+ +
+ + + + + + + + + + + + + + +
+
+ +
+
+
交易对
-
+
监控类型
-
+
方向
-
+
上沿/阻力
-
+
下沿/支撑
-
+
现价
-
+
距上沿
-
+
距下沿
-
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/crypto_monitor_binance/templates/login.html b/crypto_monitor_binance/templates/login.html index cfcc816..5bdb406 100644 --- a/crypto_monitor_binance/templates/login.html +++ b/crypto_monitor_binance/templates/login.html @@ -1,118 +1,136 @@ - - - - - 登录 · {{ exchange_display }} - - - - - - + + + + + + + 登录 · {{ exchange_display }} + + + + + + + + + diff --git a/crypto_monitor_binance/templates/order_focus_v2.html b/crypto_monitor_binance/templates/order_focus_v2.html index f9bceab..57e0a96 100644 --- a/crypto_monitor_binance/templates/order_focus_v2.html +++ b/crypto_monitor_binance/templates/order_focus_v2.html @@ -1,214 +1,231 @@ - - - - - {{ exchange_display }} | 实盘下单放大 - - - -
-
-
-
- 返回首页 - 实盘下单放大(100根K线){{ exchange_display }} -
-
最近刷新:--
-
- {% if orders %} -
- - - - - - -
- {% else %} -
当前没有激活订单,无法展示放大K线。
- {% endif %} -
- - {% if orders %} -
-
-
交易对
-
-
方向
-
-
成交价
-
-
止损
-
-
止盈
-
-
盈亏比
-
-
移动保本
-
-
现价
-
-
浮盈亏
-
-
-
- -
-
-
- {% endif %} -
- -{% if orders %} - - -{% endif %} - - - + + + + + + + {{ exchange_display }} | 实盘下单放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 实盘下单放大(100根K线){{ exchange_display }} +
+
最近刷新:--
+
+ {% if orders %} +
+ + + + + + +
+ {% else %} +
当前没有激活订单,无法展示放大K线。
+ {% endif %} +
+ + {% if orders %} +
+
+
交易对
-
+
方向
-
+
成交价
-
+
止损
-
+
止盈
-
+
盈亏比
-
+
移动保本
-
+
现价
-
+
浮盈亏
-
+
+
+ +
+
+
+ {% endif %} +
+ +{% if orders %} + + +{% endif %} + + + diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index e2dac27..abd449f 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -1,7 +1,9 @@ - + + + @@ -229,6 +231,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} + + {% macro period_stats(title, s) %} @@ -253,7 +257,21 @@

加密货币|交易监控 + AI复盘一体化

-
{{ exchange_display }}
+
+
{{ exchange_display }}
+
+ + +
+
关键位监控 diff --git a/crypto_monitor_gate/templates/key_focus_v2.html b/crypto_monitor_gate/templates/key_focus_v2.html index d4b3492..1a6a6c5 100644 --- a/crypto_monitor_gate/templates/key_focus_v2.html +++ b/crypto_monitor_gate/templates/key_focus_v2.html @@ -1,261 +1,278 @@ - - - - - {{ exchange_display }} | 关键位放大 - - - -
-
-
-
- 返回首页 - 关键位放大(可输入币种){{ exchange_display }} -
-
最近刷新:--
-
- -
- - - - - - - - - - - - - - -
-
- -
-
-
交易对
-
-
监控类型
-
-
方向
-
-
上沿/阻力
-
-
下沿/支撑
-
-
现价
-
-
距上沿
-
-
距下沿
-
-
-
- -
-
- - - - + + + + + + + {{ exchange_display }} | 关键位放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 关键位放大(可输入币种){{ exchange_display }} +
+
最近刷新:--
+
+ +
+ + + + + + + + + + + + + + +
+
+ +
+
+
交易对
-
+
监控类型
-
+
方向
-
+
上沿/阻力
-
+
下沿/支撑
-
+
现价
-
+
距上沿
-
+
距下沿
-
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/crypto_monitor_gate/templates/login.html b/crypto_monitor_gate/templates/login.html index cfcc816..5bdb406 100644 --- a/crypto_monitor_gate/templates/login.html +++ b/crypto_monitor_gate/templates/login.html @@ -1,118 +1,136 @@ - - - - - 登录 · {{ exchange_display }} - - - - - - + + + + + + + 登录 · {{ exchange_display }} + + + + + + + + + diff --git a/crypto_monitor_gate/templates/order_focus_v2.html b/crypto_monitor_gate/templates/order_focus_v2.html index f9bceab..57e0a96 100644 --- a/crypto_monitor_gate/templates/order_focus_v2.html +++ b/crypto_monitor_gate/templates/order_focus_v2.html @@ -1,214 +1,231 @@ - - - - - {{ exchange_display }} | 实盘下单放大 - - - -
-
-
-
- 返回首页 - 实盘下单放大(100根K线){{ exchange_display }} -
-
最近刷新:--
-
- {% if orders %} -
- - - - - - -
- {% else %} -
当前没有激活订单,无法展示放大K线。
- {% endif %} -
- - {% if orders %} -
-
-
交易对
-
-
方向
-
-
成交价
-
-
止损
-
-
止盈
-
-
盈亏比
-
-
移动保本
-
-
现价
-
-
浮盈亏
-
-
-
- -
-
-
- {% endif %} -
- -{% if orders %} - - -{% endif %} - - - + + + + + + + {{ exchange_display }} | 实盘下单放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 实盘下单放大(100根K线){{ exchange_display }} +
+
最近刷新:--
+
+ {% if orders %} +
+ + + + + + +
+ {% else %} +
当前没有激活订单,无法展示放大K线。
+ {% endif %} +
+ + {% if orders %} +
+
+
交易对
-
+
方向
-
+
成交价
-
+
止损
-
+
止盈
-
+
盈亏比
-
+
移动保本
-
+
现价
-
+
浮盈亏
-
+
+
+ +
+
+
+ {% endif %} +
+ +{% if orders %} + + +{% endif %} + + + diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index f914392..da29bcd 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -1,7 +1,9 @@ - + + + @@ -209,6 +211,8 @@ .stats-split-row{grid-template-columns:1fr} } + + {% macro period_metrics_cells(s) %} @@ -243,7 +247,21 @@

加密货币|Gate 机器人交易监控

-
{{ exchange_display }}
+
+
{{ exchange_display }}
+
+ + +
+
交易执行 diff --git a/crypto_monitor_gate_bot/templates/key_focus_v2.html b/crypto_monitor_gate_bot/templates/key_focus_v2.html index d4b3492..1a6a6c5 100644 --- a/crypto_monitor_gate_bot/templates/key_focus_v2.html +++ b/crypto_monitor_gate_bot/templates/key_focus_v2.html @@ -1,261 +1,278 @@ - - - - - {{ exchange_display }} | 关键位放大 - - - -
-
-
-
- 返回首页 - 关键位放大(可输入币种){{ exchange_display }} -
-
最近刷新:--
-
- -
- - - - - - - - - - - - - - -
-
- -
-
-
交易对
-
-
监控类型
-
-
方向
-
-
上沿/阻力
-
-
下沿/支撑
-
-
现价
-
-
距上沿
-
-
距下沿
-
-
-
- -
-
- - - - + + + + + + + {{ exchange_display }} | 关键位放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 关键位放大(可输入币种){{ exchange_display }} +
+
最近刷新:--
+
+ +
+ + + + + + + + + + + + + + +
+
+ +
+
+
交易对
-
+
监控类型
-
+
方向
-
+
上沿/阻力
-
+
下沿/支撑
-
+
现价
-
+
距上沿
-
+
距下沿
-
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/crypto_monitor_gate_bot/templates/login.html b/crypto_monitor_gate_bot/templates/login.html index cfcc816..5bdb406 100644 --- a/crypto_monitor_gate_bot/templates/login.html +++ b/crypto_monitor_gate_bot/templates/login.html @@ -1,118 +1,136 @@ - - - - - 登录 · {{ exchange_display }} - - - - - - + + + + + + + 登录 · {{ exchange_display }} + + + + + + + + + diff --git a/crypto_monitor_gate_bot/templates/order_focus_v2.html b/crypto_monitor_gate_bot/templates/order_focus_v2.html index 9c9add3..8d12fd6 100644 --- a/crypto_monitor_gate_bot/templates/order_focus_v2.html +++ b/crypto_monitor_gate_bot/templates/order_focus_v2.html @@ -1,214 +1,231 @@ - - - - - {{ exchange_display }} | 实盘下单放大 - - - -
-
-
-
- 返回首页 - 实盘下单放大(100根K线){{ exchange_display }} -
-
最近刷新:--
-
- {% if orders %} -
- - - - - - -
- {% else %} -
当前没有激活订单,无法展示放大K线。
- {% endif %} -
- - {% if orders %} -
-
-
交易对
-
-
方向
-
-
成交价
-
-
止损
-
-
止盈
-
-
盈亏比
-
-
移动保本
-
-
现价
-
-
浮盈亏
-
-
-
- -
-
-
- {% endif %} -
- -{% if orders %} - - -{% endif %} - - - + + + + + + + {{ exchange_display }} | 实盘下单放大 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 实盘下单放大(100根K线){{ exchange_display }} +
+
最近刷新:--
+
+ {% if orders %} +
+ + + + + + +
+ {% else %} +
当前没有激活订单,无法展示放大K线。
+ {% endif %} +
+ + {% if orders %} +
+
+
交易对
-
+
方向
-
+
成交价
-
+
止损
-
+
止盈
-
+
盈亏比
-
+
移动保本
-
+
现价
-
+
浮盈亏
-
+
+
+ +
+
+
+ {% endif %} +
+ +{% if orders %} + + +{% endif %} + + + diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index a8b442b..c96d93d 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -1,7 +1,9 @@ - + + + @@ -229,6 +231,8 @@ .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} + + {% macro period_stats(title, s) %} @@ -253,7 +257,21 @@

加密货币|交易监控 + AI复盘一体化

-
{{ exchange_display }}
+
+
{{ exchange_display }}
+
+ + +
+
关键位监控 diff --git a/crypto_monitor_okx/templates/key_focus_v2.html b/crypto_monitor_okx/templates/key_focus_v2.html index 622c7da..ffa8343 100644 --- a/crypto_monitor_okx/templates/key_focus_v2.html +++ b/crypto_monitor_okx/templates/key_focus_v2.html @@ -1,260 +1,277 @@ - - - - - 关键位放大 | K线查看 - - - -
-
-
-
- 返回首页 - 关键位放大(可输入币种) -
-
最近刷新:--
-
- -
- - - - - - - - - - - - - - -
-
- -
-
-
交易对
-
-
监控类型
-
-
方向
-
-
上沿/阻力
-
-
下沿/支撑
-
-
现价
-
-
距上沿
-
-
距下沿
-
-
-
- -
-
- - - - + + + + + + + 关键位放大 | K线查看 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 关键位放大(可输入币种) +
+
最近刷新:--
+
+ +
+ + + + + + + + + + + + + + +
+
+ +
+
+
交易对
-
+
监控类型
-
+
方向
-
+
上沿/阻力
-
+
下沿/支撑
-
+
现价
-
+
距上沿
-
+
距下沿
-
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/crypto_monitor_okx/templates/login.html b/crypto_monitor_okx/templates/login.html index f31ba84..1711d83 100644 --- a/crypto_monitor_okx/templates/login.html +++ b/crypto_monitor_okx/templates/login.html @@ -1,107 +1,125 @@ - - - - - 系统登录 - - - - - - + + + + + + + 系统登录 + + + + + + + + + diff --git a/crypto_monitor_okx/templates/order_focus_v2.html b/crypto_monitor_okx/templates/order_focus_v2.html index 582eaa0..8341a5e 100644 --- a/crypto_monitor_okx/templates/order_focus_v2.html +++ b/crypto_monitor_okx/templates/order_focus_v2.html @@ -1,211 +1,228 @@ - - - - - 实盘下单放大 | 100根K线 - - - -
-
-
-
- 返回首页 - 实盘下单放大(100根K线) -
-
最近刷新:--
-
- {% if orders %} -
- - - - - - -
- {% else %} -
当前没有激活订单,无法展示放大K线。
- {% endif %} -
- - {% if orders %} -
-
-
交易对
-
-
方向
-
-
成交价
-
-
止损
-
-
止盈
-
-
盈亏比
-
-
现价
-
-
浮盈亏
-
-
-
- -
-
-
- {% endif %} -
- -{% if orders %} - - -{% endif %} - - - + + + + + + + 实盘下单放大 | 100根K线 + + + + + +
+
+
+
+ + +
+ +
+ 返回首页 + 实盘下单放大(100根K线) +
+
最近刷新:--
+
+ {% if orders %} +
+ + + + + + +
+ {% else %} +
当前没有激活订单,无法展示放大K线。
+ {% endif %} +
+ + {% if orders %} +
+
+
交易对
-
+
方向
-
+
成交价
-
+
止损
-
+
止盈
-
+
盈亏比
-
+
现价
-
+
浮盈亏
-
+
+
+ +
+
+
+ {% endif %} +
+ +{% 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", + 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 = '
' + if marker in text: + text = text.replace( + marker, + marker + "\n " + THEME_TOGGLE.replace("\n", "\n "), + 1, + ) + if text != orig: + path.write_text(text, encoding="utf-8") + return True + return False + + +def main() -> None: + n = 0 + for ex in EXCHANGES: + for fn in FILES: + p = ROOT / ex / "templates" / fn + if p.is_file() and patch_file(p): + print("patched", p.relative_to(ROOT)) + n += 1 + print("done", n, "files") + + +if __name__ == "__main__": + main() diff --git a/static/instance_theme.css b/static/instance_theme.css new file mode 100644 index 0000000..c08e0b6 --- /dev/null +++ b/static/instance_theme.css @@ -0,0 +1,160 @@ +/* 实例页亮色主题(覆盖模板内联暗色样式) */ +html[data-theme="light"] { + color-scheme: light; +} + +html[data-theme="light"] body { + background: #d8e2ec !important; + color: #1a2838 !important; +} + +html[data-theme="light"] .header h1 { + color: #142232 !important; +} + +html[data-theme="light"] .exchange-tag { + color: #087a50 !important; + background: rgba(10, 143, 92, 0.12) !important; + border-color: rgba(10, 143, 92, 0.35) !important; +} + +html[data-theme="light"] .top-nav a { + background: #fff !important; + color: #006e9a !important; + border-color: rgba(0, 95, 140, 0.22) !important; +} + +html[data-theme="light"] .top-nav a.active { + background: rgba(0, 110, 154, 0.12) !important; + color: #142232 !important; +} + +html[data-theme="light"] .stat-item, +html[data-theme="light"] .card, +html[data-theme="light"] .meta-item, +html[data-theme="light"] .list-item { + background: #fff !important; + border-color: #b8c8d8 !important; +} + +html[data-theme="light"] .stat-item .label, +html[data-theme="light"] .status, +html[data-theme="light"] .rule-tip { + color: #4a6078 !important; +} + +html[data-theme="light"] .stat-item .value, +html[data-theme="light"] .card h2 { + color: #142232 !important; +} + +html[data-theme="light"] input, +html[data-theme="light"] select, +html[data-theme="light"] textarea { + background: #f6f9fc !important; + color: #142232 !important; + border-color: #b8c8d8 !important; +} + +html[data-theme="light"] .flash { + background: rgba(0, 110, 154, 0.1) !important; + color: #006e9a !important; + border-color: rgba(0, 95, 140, 0.22) !important; +} + +html[data-theme="light"] th { + color: #4a6078 !important; +} + +html[data-theme="light"] td { + color: #142232 !important; + border-bottom-color: #d0dae4 !important; +} + +html[data-theme="light"] .ai-result, +html[data-theme="light"] .login-box { + background: #fff !important; + border-color: #b8c8d8 !important; + color: #142232 !important; +} + +html[data-theme="light"] #chart-wrap { + background: #f0f4f9 !important; + border-color: #b8c8d8 !important; +} + +html[data-theme="light"] .btn { + background: #fff !important; + color: #006e9a !important; + border-color: rgba(0, 95, 140, 0.22) !important; +} + +html[data-theme="light"] .btn:hover { + background: #eef3f8 !important; +} + +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 3px; + border-radius: 8px; + border: 1px solid #304164; + background: #151a2a; +} + +html[data-theme="light"] .theme-toggle { + background: #fff; + border-color: #b8c8d8; +} + +.theme-toggle.is-hub-linked { + display: none !important; +} + +.theme-toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 30px; + padding: 0; + border: none; + border-radius: 6px; + background: transparent; + color: #8fc8ff; + cursor: pointer; +} + +html[data-theme="light"] .theme-toggle-btn { + color: #4a6078; +} + +.theme-toggle-btn.is-active { + color: #dbe4ff; + background: rgba(79, 121, 255, 0.2); + box-shadow: inset 0 0 0 1px #304164; +} + +html[data-theme="light"] .theme-toggle-btn.is-active { + color: #006e9a; + background: rgba(0, 110, 154, 0.12); + box-shadow: inset 0 0 0 1px #b8c8d8; +} + +.header-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 6px; +} + +.login-theme-bar { + display: flex; + justify-content: flex-end; + width: 100%; + max-width: 400px; + margin: 0 auto 10px; +} diff --git a/static/instance_theme.js b/static/instance_theme.js new file mode 100644 index 0000000..afddfea --- /dev/null +++ b/static/instance_theme.js @@ -0,0 +1,141 @@ +/** + * 四所实例主题:默认暗色;单独登录用 instance-theme;中控 iframe/SSO 随 hub-theme 联动。 + */ +(function (global) { + const STANDALONE_KEY = "instance-theme"; + const META = { dark: "#0b0d14", light: "#d8e2ec" }; + + function normalize(theme) { + return theme === "light" ? "light" : "dark"; + } + + function isHubLinked() { + try { + const p = new URLSearchParams(location.search); + if (p.get("embed") === "1") return true; + const ht = p.get("hub_theme"); + if (ht === "light" || ht === "dark") return true; + } catch (_) {} + try { + if (window.self !== window.top) return true; + } catch (_) { + return true; + } + return false; + } + + function themeFromUrl() { + try { + const t = new URLSearchParams(location.search).get("hub_theme"); + if (t === "light" || t === "dark") return t; + } catch (_) {} + return null; + } + + function getStandalone() { + try { + return normalize(localStorage.getItem(STANDALONE_KEY)); + } catch (_) { + return "dark"; + } + } + + function setStandalone(theme) { + try { + localStorage.setItem(STANDALONE_KEY, normalize(theme)); + } catch (_) {} + } + + function get() { + if (isHubLinked()) { + return themeFromUrl() || _linkedTheme || "dark"; + } + return getStandalone(); + } + + let _linkedTheme = null; + + function apply(theme, opts) { + const options = opts || {}; + const linked = isHubLinked(); + const t = normalize(theme); + if (linked) { + _linkedTheme = t; + } else if (!options.skipStore) { + setStandalone(t); + } + const root = document.documentElement; + root.setAttribute("data-theme", t); + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) meta.setAttribute("content", META[t]); + root.style.colorScheme = t; + syncToggleUI(); + document.dispatchEvent( + new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } }) + ); + return t; + } + + function syncToggleUI(root) { + const scope = root || document; + const linked = isHubLinked(); + const toggle = scope.querySelector(".instance-theme-toggle"); + if (toggle) { + toggle.classList.toggle("is-hub-linked", linked); + toggle.setAttribute("aria-hidden", linked ? "true" : "false"); + } + if (linked) return; + scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => { + const on = btn.getAttribute("data-theme-value") === getStandalone(); + btn.classList.toggle("is-active", on); + btn.setAttribute("aria-pressed", on ? "true" : "false"); + }); + } + + function initToggleUI(root) { + const scope = root || document; + syncToggleUI(scope); + scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => { + if (btn.dataset.themeBound === "1") return; + btn.dataset.themeBound = "1"; + btn.addEventListener("click", () => { + if (isHubLinked()) return; + apply(btn.getAttribute("data-theme-value")); + }); + }); + } + + function initFromHubMessage(data) { + if (!data || data.type !== "hub-theme-sync") return; + if (!isHubLinked()) return; + apply(data.theme, { skipStore: true }); + } + + function boot() { + if (isHubLinked()) { + apply(themeFromUrl() || "dark", { skipStore: true }); + window.addEventListener("message", (ev) => initFromHubMessage(ev.data)); + try { + window.parent.postMessage({ type: "instance-theme-ready" }, "*"); + } catch (_) {} + } else { + apply(getStandalone()); + } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => initToggleUI()); + } else { + initToggleUI(); + } + } + + boot(); + + global.InstanceTheme = { + STANDALONE_KEY, + isHubLinked, + get, + apply, + initToggleUI, + syncToggleUI, + }; +})(typeof window !== "undefined" ? window : globalThis);