From 22de1fd40c5ac7e9a90d21802e21019083964cda Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 19:17:00 +0800 Subject: [PATCH] Match options branding: title prefix and ETH/BTC header quotes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use 「加密货币 | OKX 对冲」 and show live top-right ETHUSDT/BTCUSDT quotes like the options instance. Co-authored-by: Cursor --- app.py | 38 ++++++ lib/common/static/header_live_quotes.js | 107 +++++++++++++++ lib/common/static/instance_theme.css | 123 ++++++++++++++++++ lib/hub/hub_bridge.py | 2 + lib/instance/instance_embed_lib.py | 2 +- lib/instance/templates/embed_shell.html | 7 +- lib/instance/templates/index.html | 7 +- .../templates/instance_brand_header.html | 24 ++++ 8 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 lib/common/static/header_live_quotes.js create mode 100644 lib/instance/templates/instance_brand_header.html diff --git a/app.py b/app.py index 7bf0bb1..5ef8d3c 100644 --- a/app.py +++ b/app.py @@ -7134,6 +7134,36 @@ def options_main_page(): return render_main_page("options") +@app.route("/api/header-quotes") +@login_required +def api_header_quotes(): + """顶栏 ETH/BTC 报价(公共 ticker last + 24h 涨跌).""" + from lib.exchange.okx_public_http_lib import fetch_swap_ticker + + quotes = [] + for base in ("ETH", "BTC"): + item: dict = {"base": base, "symbol": f"{base}USDT"} + try: + row = fetch_swap_ticker(base) + last = float(row.get("last") or row.get("markPx") or 0) + open24 = float(row.get("open24h") or row.get("sodUtc0") or 0) + change = (last - open24) if open24 > 0 else 0.0 + pct = (change / open24 * 100.0) if open24 > 0 else 0.0 + item.update( + { + "last": last, + "open24h": open24, + "change": change, + "pct": pct, + "mark": float(row.get("markPx") or last or 0) or None, + } + ) + except Exception as e: + item["error"] = str(e)[:160] + quotes.append(item) + return jsonify({"ok": True, "quotes": quotes}) + + @app.route("/api/account_snapshot") @login_required def api_account_snapshot(): @@ -9408,6 +9438,7 @@ _OPTIONS_PANEL_JS = os.path.join(_REPO_STATIC_DIR, "options_panel.js") _OPTIONS_EXPIRY_COUNTDOWN_JS = os.path.join(_REPO_STATIC_DIR, "options_expiry_countdown.js") _OPTIONS_SETTINGS_JS = os.path.join(_REPO_STATIC_DIR, "options_settings.js") _HEDGE_PLAN_JS = os.path.join(_REPO_STATIC_DIR, "hedge_plan.js") +_HEADER_LIVE_QUOTES_JS = os.path.join(_REPO_STATIC_DIR, "header_live_quotes.js") @app.route("/static/ai_review_render.js") @@ -9417,6 +9448,13 @@ def static_ai_review_render_js(): return send_file(_AI_REVIEW_RENDER_JS, mimetype="application/javascript; charset=utf-8") +@app.route("/static/header_live_quotes.js") +def static_header_live_quotes_js(): + if not os.path.isfile(_HEADER_LIVE_QUOTES_JS): + return Response("not found", status=404, mimetype="text/plain; charset=utf-8") + return send_file(_HEADER_LIVE_QUOTES_JS, mimetype="application/javascript; charset=utf-8") + + @app.route("/static/form_submit_guard.js") def static_form_submit_guard_js(): if not os.path.isfile(_FORM_SUBMIT_GUARD_JS): diff --git a/lib/common/static/header_live_quotes.js b/lib/common/static/header_live_quotes.js new file mode 100644 index 0000000..fe606d3 --- /dev/null +++ b/lib/common/static/header_live_quotes.js @@ -0,0 +1,107 @@ +/** + * 顶栏右上角 ETH/BTC 实时报价(公共 ticker). + */ +(function (global) { + const BASES = ["ETH", "BTC"]; + + function $(id) { + return document.getElementById(id); + } + + function refreshMs() { + const raw = document.body && document.body.getAttribute("data-price-refresh-ms"); + const n = Number(raw); + return n > 2000 ? n : 5000; + } + + function fmtLast(base, v) { + if (v == null || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + if (base === "BTC") { + return n.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); + } + return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function fmtChg(base, v) { + if (v == null || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const abs = Math.abs(n); + const body = + base === "BTC" + ? abs.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + : abs.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + if (n > 0) return "+" + body; + if (n < 0) return "-" + body; + return body; + } + + function fmtPct(v) { + if (v == null || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const body = Math.abs(n).toFixed(2) + "%"; + if (n > 0) return "+" + body; + if (n < 0) return "-" + body; + return body; + } + + function paintRow(row, q) { + if (!row || !q) return; + const last = row.querySelector('[data-hlq="last"]'); + const chg = row.querySelector('[data-hlq="chg"]'); + const pct = row.querySelector('[data-hlq="pct"]'); + const base = row.getAttribute("data-base") || ""; + if (last) last.textContent = fmtLast(base, q.last); + if (chg) chg.textContent = fmtChg(base, q.change); + if (pct) pct.textContent = fmtPct(q.pct); + const up = Number(q.change) > 0 || Number(q.pct) > 0; + const down = Number(q.change) < 0 || Number(q.pct) < 0; + row.classList.toggle("is-up", up); + row.classList.toggle("is-down", down); + } + + async function fetchQuotes() { + const r = await fetch("/api/header-quotes", { + credentials: "same-origin", + cache: "no-store", + }); + const d = await r.json(); + if (!d || !d.ok) throw new Error((d && d.msg) || "行情失败"); + return d.quotes || []; + } + + async function tick() { + const host = $("header-live-quotes"); + if (!host) return; + try { + const quotes = await fetchQuotes(); + BASES.forEach(function (base) { + const row = host.querySelector('.hlq-row[data-base="' + base + '"]'); + const q = quotes.find(function (x) { + return String((x && x.base) || "").toUpperCase() === base; + }); + if (row && q && !q.error) paintRow(row, q); + }); + } catch (_) { + /* 静默:保持上次有效行情 */ + } + } + + let timer = null; + function boot() { + if (!$("header-live-quotes")) return; + void tick(); + if (timer) clearInterval(timer); + timer = setInterval(function () { + void tick(); + }, refreshMs()); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } + + global.HeaderLiveQuotes = { refresh: tick, boot: boot }; +})(window); diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index bf2b47a..5c45a6a 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -561,6 +561,129 @@ html[data-theme="light"] .theme-toggle-btn.is-active { margin-top: 6px; } +/* 顶栏品牌:加密货币 | OKX 对冲 + 右上角 ETH/BTC 报价 */ +.header.header--brand { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 52px; + margin-bottom: 10px; + padding: 0 168px 0 12px; + box-sizing: border-box; +} +.header.header--brand .brand-title { + margin: 0; + font-size: 1.7rem; + font-weight: 700; + color: #e8ecff; + text-align: center; + line-height: 1.25; + letter-spacing: 0.02em; +} +.header.header--brand .brand-prefix { + color: #e8ecff; +} +.header.header--brand .brand-sep { + display: inline-block; + margin: 0 0.45em; + color: #9aa3bf; + font-weight: 500; +} +.header.header--brand .brand-product { + color: #e8ecff; +} +.header-live-quotes { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + z-index: 2; + pointer-events: none; +} +.header-live-quotes .hlq-row { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.72rem; + line-height: 1.2; + color: #c5cce0; + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.header-live-quotes .hlq-icon { + width: 14px; + height: 14px; + border-radius: 50%; + flex: 0 0 auto; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); +} +.header-live-quotes .hlq-icon--eth { + background: radial-gradient(circle at 35% 30%, #8ad0ff, #3c7cff 55%, #1a3a8a); +} +.header-live-quotes .hlq-icon--btc { + background: radial-gradient(circle at 35% 30%, #ffd27a, #f0a028 55%, #a65a00); +} +.header-live-quotes .hlq-sym { + color: #9aa3bf; + min-width: 58px; +} +.header-live-quotes .hlq-last { + color: #e8ecff; + font-weight: 600; + min-width: 64px; + text-align: right; +} +.header-live-quotes .hlq-chg, +.header-live-quotes .hlq-pct { + min-width: 48px; + text-align: right; + color: #9aa3bf; +} +.header-live-quotes .hlq-row.is-up .hlq-chg, +.header-live-quotes .hlq-row.is-up .hlq-pct { + color: #3dd68c; +} +.header-live-quotes .hlq-row.is-down .hlq-chg, +.header-live-quotes .hlq-row.is-down .hlq-pct { + color: #ff8a8a; +} +html[data-theme="light"] .header.header--brand .brand-title, +html[data-theme="light"] .header.header--brand .brand-prefix, +html[data-theme="light"] .header.header--brand .brand-product { + color: #142232; +} +html[data-theme="light"] .header.header--brand .brand-sep, +html[data-theme="light"] .header-live-quotes .hlq-sym { + color: #5a6f85; +} +html[data-theme="light"] .header-live-quotes .hlq-last { + color: #142232; +} +@media (max-width: 720px) { + .header.header--brand { + padding: 0 8px; + min-height: 0; + align-items: flex-start; + } + .header.header--brand .brand-title { + font-size: 1.05rem !important; + text-align: left; + } + .header-live-quotes { + position: static; + transform: none; + margin-top: 6px; + align-items: flex-start; + width: 100%; + } +} + /* ── 统一顶栏面板:状态/筛选 + 统计条 ── */ .instance-header-panel { margin-top: 18px; diff --git a/lib/hub/hub_bridge.py b/lib/hub/hub_bridge.py index 2c3ffa4..73d8f41 100644 --- a/lib/hub/hub_bridge.py +++ b/lib/hub/hub_bridge.py @@ -79,6 +79,8 @@ def install_instance_theme_static(app) -> None: "account_ledger.js": "application/javascript; charset=utf-8", "options_expiry_countdown.js": "application/javascript; charset=utf-8", "options_panel.js": "application/javascript; charset=utf-8", + "header_live_quotes.js": "application/javascript; charset=utf-8", + "hedge_plan.js": "application/javascript; charset=utf-8", "order_entry_model.js": "application/javascript; charset=utf-8", "focus_chart_page.js": "application/javascript; charset=utf-8", "focus_chart_page.css": "text/css; charset=utf-8", diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py index 9282e4c..e6523e1 100644 --- a/lib/instance/instance_embed_lib.py +++ b/lib/instance/instance_embed_lib.py @@ -215,7 +215,7 @@ def pwa_app_name(exchange_key: str) -> str: ex = (exchange_key or "").strip().lower() return { "binance": "Binance 交易系统", - "okx": "okx对冲", + "okx": "加密货币 | OKX 对冲", "gate": "Gate 交易系统", }.get(ex, "交易系统") diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 198909f..25ad49b 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -8,7 +8,7 @@ - + @@ -27,9 +27,7 @@ data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}" >
-
-

okx对冲

-
+ {% include 'instance_brand_header.html' %}