From 8838f1f2210d2d8fc239d833b147e4330250c7c1 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 15 Jul 2026 22:56:02 +0800 Subject: [PATCH] Phone hub folds: emergency ops, market toolbar, calc tabs, OKX options. Collapse bulky monitor/market controls by default on phone, show OKX options on tiles, add calculator tab switch, and request landscape on market fullscreen. Co-authored-by: Cursor --- manual_trading_hub/static/app.css | 151 ++++++++++++++++++++++++ manual_trading_hub/static/app.js | 128 ++++++++++++++++---- manual_trading_hub/static/calculator.js | 32 +++++ manual_trading_hub/static/chart.js | 30 +++++ manual_trading_hub/static/index.html | 118 ++++++++++-------- 5 files changed, 387 insertions(+), 72 deletions(-) diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 2261877..e8f0cea 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -4035,6 +4035,38 @@ body.login-page { } } +/* —— 可折叠条:桌面/平板始终展开(隐藏 summary) —— */ +.hub-m-fold { + margin: 0 0 12px; + border: none; + background: transparent; + padding: 0; +} + +.hub-m-fold > .hub-m-fold-summary { + display: none; + list-style: none; +} + +.hub-m-fold > .hub-m-fold-summary::-webkit-details-marker { + display: none; +} + +.calc-mobile-tabs { + display: none; +} + +.card-stat-chip.card-stat-options { + border-color: rgba(0, 212, 255, 0.35); + color: var(--accent, #6ea8ff); + background: rgba(0, 212, 255, 0.08); +} + +.hub-tile-opt { + color: var(--accent, #6ea8ff) !important; + font-weight: 500; +} + /* —— 手机壳:底栏四件套(仅 ≤720px;桌面/平板完全不进此规则) —— */ .hub-mobile-tabbar, .hub-mobile-more { @@ -4370,6 +4402,125 @@ body.login-page { font-size: 13px; } + /* 手机折叠条 */ + body.hub-phone .hub-m-fold { + margin: 0 0 10px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--panel); + overflow: hidden; + } + + body.hub-phone .hub-m-fold > .hub-m-fold-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: var(--text); + -webkit-tap-highlight-color: transparent; + } + + body.hub-phone .hub-m-fold > .hub-m-fold-summary::after { + content: "▾"; + color: var(--muted); + font-size: 12px; + transition: transform 0.15s; + } + + body.hub-phone .hub-m-fold[open] > .hub-m-fold-summary::after { + transform: rotate(180deg); + } + + body.hub-phone .hub-m-fold-meta { + flex: 1 1 auto; + min-width: 0; + text-align: right; + font-size: 10px; + font-weight: 500; + font-family: var(--mono); + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + body.hub-phone .hub-m-fold .toolbar, + body.hub-phone .hub-m-fold .market-toolbar { + margin-bottom: 0; + border: none; + border-top: 1px solid var(--border-soft); + border-radius: 0; + box-shadow: none; + } + + /* 计算器手机 Tab */ + body.hub-phone .calc-mobile-tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + margin: 0 0 10px; + } + + body.hub-phone .calc-m-tab { + min-height: 38px; + border: 1px solid var(--border-soft); + border-radius: 10px; + background: var(--nav-bg, rgba(255, 255, 255, 0.03)); + color: var(--muted); + font: inherit; + font-size: 13px; + font-weight: 600; + cursor: pointer; + } + + body.hub-phone .calc-m-tab.is-active { + color: var(--accent, #6ea8ff); + border-color: color-mix(in srgb, var(--accent, #6ea8ff) 45%, var(--border-soft)); + background: var(--accent-dim, rgba(110, 168, 255, 0.12)); + } + + body.hub-phone .calc-layout[data-calc-tab="trend"] [data-calc-pane="roll"], + body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"] { + display: none; + } + + body.hub-phone .calc-layout { + grid-template-columns: 1fr; + } + + body.hub-phone #page-calculator .calc-card h2 { + display: none; + } + + /* 行情全屏:竖屏提示转横;横屏吃满 */ + @media (orientation: portrait) { + body.hub-phone.market-chart-fs-open .market-chart-wrap.is-fullscreen::before { + content: "全屏看图请横持手机"; + position: absolute; + top: max(10px, env(safe-area-inset-top)); + left: 50%; + transform: translateX(-50%); + z-index: 5; + padding: 6px 12px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.72); + color: #fff; + font-size: 12px; + pointer-events: none; + } + } + + @media (orientation: landscape) { + body.hub-phone.market-chart-fs-open .app-header, + body.hub-phone.market-chart-fs-open .hub-mobile-tabbar { + display: none !important; + } + } + .hub-mobile-tabbar { display: flex; position: fixed; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 1c5930c..ffcd258 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -1493,15 +1493,18 @@ else pill.classList.remove("syncing"); } const upd = document.getElementById("monitor-updated"); - if (upd) { + const updSum = document.getElementById("monitor-updated-summary"); + if (upd || updSum) { const ts = tsRaw.replace("T", " "); - upd.textContent = options.stale + const txt = options.stale ? ts ? `缓存 ${ts} · 后台聚合中…` : "后台聚合中…" : ts ? `UPD ${ts}` : ""; + if (upd) upd.textContent = txt; + if (updSum) updSum.textContent = txt; } updateMonitorAlertSummary(rows || []); void refreshMacroRiskBanner(rows || []); @@ -1923,6 +1926,46 @@ syncHubAiMobileViewport(); } + function syncHubPhoneFolds() { + const phone = isMobileLayout(); + ["monitor-ops-fold", "market-toolbar-fold"].forEach((id) => { + const el = document.getElementById(id); + if (!el) return; + if (!phone) { + el.open = true; + return; + } + // 手机默认折叠;用户手动点开后本会话不再强关 + if (el.dataset.userToggled === "1") return; + el.open = false; + }); + } + + function bindHubPhoneFolds() { + ["monitor-ops-fold", "market-toolbar-fold"].forEach((id) => { + const el = document.getElementById(id); + if (!el || el.dataset.boundFold === "1") return; + el.dataset.boundFold = "1"; + el.addEventListener("toggle", () => { + if (!isMobileLayout()) return; + el.dataset.userToggled = "1"; + }); + }); + const sym = document.getElementById("market-symbol"); + const tf = document.getElementById("market-timeframe"); + const syncMarketFoldMeta = () => { + const meta = document.getElementById("market-fold-summary"); + if (!meta) return; + const s = sym ? String(sym.value || "").trim() : ""; + const t = tf ? String(tf.value || "").trim() : ""; + meta.textContent = [s || "—", t || "—"].join(" · "); + }; + if (sym) sym.addEventListener("change", syncMarketFoldMeta); + if (sym) sym.addEventListener("input", syncMarketFoldMeta); + if (tf) tf.addEventListener("change", syncMarketFoldMeta); + syncMarketFoldMeta(); + } + function initHubMobileChrome() { const moreBtn = document.getElementById("m-tab-more"); const backdrop = document.getElementById("hub-mobile-more-backdrop"); @@ -1940,7 +1983,9 @@ closeHubMobileMore(); } }); + bindHubPhoneFolds(); syncHubPhoneShellClass(); + syncHubPhoneFolds(); } function bindHubSpaNavLinks(selector) { @@ -1972,6 +2017,7 @@ resizeTimer = setTimeout(() => { const nowMobile = isMobileLayout(); syncHubPhoneShellClass(); + syncHubPhoneFolds(); if (!nowMobile) closeHubMobileMore(); if (lastMonitorRows.length && nowMobile !== wasMobile) { wasMobile = nowMobile; @@ -3541,21 +3587,37 @@ } function renderCardStrategyStats(row, hm, flaskOk) { - if (!flaskOk || !hm || typeof hm !== "object") return ""; const caps = row.capabilities || []; const chips = []; - if (caps.includes("key")) { - const kc = countKeyMonitorsByBucket(hm.keys || []); - if (kc.breakout > 0) chips.push({ kind: "key-breakout", label: `突破 ${kc.breakout}` }); - if (kc.fib > 0) chips.push({ kind: "key-breakout", label: `斐波 ${kc.fib}` }); - if (kc.watch > 0) chips.push({ kind: "key-watch", label: `监控 ${kc.watch}` }); + if (flaskOk && hm && typeof hm === "object") { + if (caps.includes("key")) { + const kc = countKeyMonitorsByBucket(hm.keys || []); + if (kc.breakout > 0) chips.push({ kind: "key-breakout", label: `突破 ${kc.breakout}` }); + if (kc.fib > 0) chips.push({ kind: "key-breakout", label: `斐波 ${kc.fib}` }); + if (kc.watch > 0) chips.push({ kind: "key-watch", label: `监控 ${kc.watch}` }); + } + if (caps.includes("trend")) { + const trendN = Array.isArray(hm.trends) ? hm.trends.length : 0; + if (trendN > 0) chips.push({ kind: "trend", label: `趋势回调 ${trendN}` }); + } + const rollN = Array.isArray(hm.rolls) ? hm.rolls.length : 0; + if (rollN > 0) chips.push({ kind: "roll", label: `顺势加仓 ${rollN}` }); } - if (caps.includes("trend")) { - const trendN = Array.isArray(hm.trends) ? hm.trends.length : 0; - if (trendN > 0) chips.push({ kind: "trend", label: `趋势回调 ${trendN}` }); + // OKX/已勾选期权能力:磁贴始终展示期权入口态(含 0 仓) + if (caps.includes("options") || rowHasOptionsLayout(row)) { + const opt = row.options || {}; + if (opt.enabled === false) { + chips.push({ kind: "options", label: "期权 · 未启用" }); + } else if (opt.ok === false) { + chips.push({ kind: "options", label: "期权 · 异常" }); + } else { + const n = Number( + opt.position_count != null ? opt.position_count : (opt.positions || []).length + ); + const nSafe = Number.isFinite(n) ? n : 0; + chips.push({ kind: "options", label: nSafe > 0 ? `期权 ${nSafe}仓` : "期权" }); + } } - const rollN = Array.isArray(hm.rolls) ? hm.rolls.length : 0; - if (rollN > 0) chips.push({ kind: "roll", label: `顺势加仓 ${rollN}` }); if (!chips.length) return ""; return `
${chips .map( @@ -4110,12 +4172,33 @@ const posLine = openCount > 0 ? `${openCount}仓 · ${alert.summary}` : alert.summary; const opt = row.options || {}; - const optCount = - opt.enabled !== false && opt.ok !== false - ? Number(opt.position_count != null ? opt.position_count : (opt.positions || []).length) - : 0; - const posLineWithOpt = - optCount > 0 ? `${posLine} · 期权${optCount}仓` : posLine; + const hasOptCap = (row.capabilities || []).includes("options") || rowHasOptionsLayout(row); + let optLine = ""; + let pnlShow = upnl; + let pnlSuffix = ""; + if (hasOptCap) { + if (opt.enabled === false) { + optLine = "期权未启用"; + } else if (opt.ok === false) { + optLine = "期权异常"; + } else { + const optCount = Number( + opt.position_count != null ? opt.position_count : (opt.positions || []).length + ); + const n = Number.isFinite(optCount) ? optCount : 0; + const bal = typeof optionsBalanceFields === "function" ? optionsBalanceFields(opt) : {}; + const optUpl = bal && bal.upl != null ? bal.upl : null; + optLine = n > 0 ? `期权 ${n}仓` : "期权 空仓"; + if (optUpl != null && Number.isFinite(Number(optUpl))) { + optLine += ` · 浮盈 ${fmt(optUpl, 2)}U`; + // 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U + if (openCount === 0) { + pnlShow = optUpl; + pnlSuffix = "期权"; + } + } + } + } const hm = row.hub_monitor || {}; const flaskOk = row.flask_ok !== false && hm.ok !== false; const strategyStats = renderCardStrategyStats(row, hm, flaskOk); @@ -4128,10 +4211,13 @@
${ showAccountPnlPref() - ? `
${fmt(upnl, 2)} U
` + ? `
${fmt(pnlShow, 2)} U${ + pnlSuffix ? " · " + pnlSuffix : "" + }
` : "" } -
${esc(posLineWithOpt)}
+
${esc(posLine)}
+ ${optLine ? `
${esc(optLine)}
` : ""} ${strategyStats}
UPD ${esc(tsShort)}
diff --git a/manual_trading_hub/static/calculator.js b/manual_trading_hub/static/calculator.js index e93fb8b..b542d71 100644 --- a/manual_trading_hub/static/calculator.js +++ b/manual_trading_hub/static/calculator.js @@ -572,6 +572,37 @@ } } + function applyCalcTab(tab) { + const t = tab === "roll" ? "roll" : "trend"; + const layout = page.querySelector(".calc-layout"); + if (layout) layout.setAttribute("data-calc-tab", t); + page.querySelectorAll(".calc-m-tab").forEach(function (btn) { + const on = (btn.getAttribute("data-calc-tab") || "") === t; + btn.classList.toggle("is-active", on); + btn.setAttribute("aria-selected", on ? "true" : "false"); + }); + try { + sessionStorage.setItem("hub_calc_tab", t); + } catch (e) { + /* ignore */ + } + } + + function bindCalcTabs() { + page.querySelectorAll(".calc-m-tab").forEach(function (btn) { + btn.addEventListener("click", function () { + applyCalcTab(btn.getAttribute("data-calc-tab") || "trend"); + }); + }); + let saved = "trend"; + try { + saved = sessionStorage.getItem("hub_calc_tab") || "trend"; + } catch (e) { + saved = "trend"; + } + applyCalcTab(saved); + } + async function bindOnce() { if (inited) return; inited = true; @@ -588,6 +619,7 @@ bindRollLegsUI(); bindMarket("calc-trend"); bindMarket("calc-roll"); + bindCalcTabs(); const refreshBtn = $("calc-btn-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", function () { diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index 3de1738..debebd1 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -1168,6 +1168,30 @@ elFsExchange.value = elExchange.value; } + function unlockMarketFsOrientation() { + try { + if (screen.orientation && typeof screen.orientation.unlock === "function") { + screen.orientation.unlock(); + } + } catch (e) { + /* ignore */ + } + document.body.classList.remove("market-fs-want-landscape"); + } + + function lockMarketFsLandscape() { + document.body.classList.add("market-fs-want-landscape"); + const orient = screen.orientation; + if (!orient || typeof orient.lock !== "function") return; + Promise.resolve(orient.lock("landscape")) + .catch(function () { + return orient.lock("landscape-primary"); + }) + .catch(function () { + /* 浏览器可能拒绝;CSS 会提示转横屏 */ + }); + } + function setChartFullscreen(on) { chartFullscreen = !!on; const wrap = elChartWrap || (chartHost && chartHost.closest(".market-chart-wrap")); @@ -1183,6 +1207,12 @@ if (chartFullscreen) { populateFsExchangeOptions(); syncFsToolbarFromMain(); + // 手机全屏优先横屏看图;电脑/平板不强制 + if (window.matchMedia && window.matchMedia("(max-width: 720px)").matches) { + lockMarketFsLandscape(); + } + } else { + unlockMarketFsOrientation(); } scheduleChartResize(); } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index b6583a1..c916fe5 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,7 +15,7 @@ - + @@ -235,15 +235,21 @@ -
- - - - - -
+
+ + 操作 · 刷新 / 紧急全平 + + +
+ + + + + +
+
@@ -251,42 +257,48 @@

MKT 行情区

-
- -