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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-15 22:56:02 +08:00
parent f16dcac202
commit 8838f1f221
5 changed files with 387 additions and 72 deletions
+151
View File
@@ -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;
+107 -21
View File
@@ -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 `<div class="card-strategy-stats">${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 @@
</div>
${
showAccountPnlPref()
? `<div class="hub-tile-pnl ${pnlCls(upnl)}">${fmt(upnl, 2)} <small>U</small></div>`
? `<div class="hub-tile-pnl ${pnlCls(pnlShow)}">${fmt(pnlShow, 2)} <small>U${
pnlSuffix ? " · " + pnlSuffix : ""
}</small></div>`
: ""
}
<div class="hub-tile-meta">${esc(posLineWithOpt)}</div>
<div class="hub-tile-meta">${esc(posLine)}</div>
${optLine ? `<div class="hub-tile-meta hub-tile-opt">${esc(optLine)}</div>` : ""}
${strategyStats}
<div class="hub-tile-foot">UPD ${esc(tsShort)}</div>
</div>
+32
View File
@@ -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 () {
+30
View File
@@ -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();
}
+67 -51
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260715-hub-phone-clean" />
<link rel="stylesheet" href="/assets/app.css?v=20260715-hub-phone-folds" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -235,15 +235,21 @@
</div>
</div>
<div id="monitor-alert-summary" class="monitor-alert-summary hidden" aria-live="polite"></div>
<div class="toolbar">
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
<label class="chk-label">
<input type="checkbox" id="auto-monitor" checked /> 后台每 5 秒聚合 · SSE 自动更新
</label>
<button type="button" id="btn-close-all" class="danger">全局紧急全平</button>
<span class="toolbar-spacer"></span>
<span id="monitor-updated" class="toolbar-meta"></span>
</div>
<details id="monitor-ops-fold" class="hub-m-fold monitor-ops-fold">
<summary class="hub-m-fold-summary">
<span class="hub-m-fold-title">操作 · 刷新 / 紧急全平</span>
<span id="monitor-updated-summary" class="hub-m-fold-meta"></span>
</summary>
<div class="toolbar">
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
<label class="chk-label">
<input type="checkbox" id="auto-monitor" checked /> 后台每 5 秒聚合 · SSE 自动更新
</label>
<button type="button" id="btn-close-all" class="danger">全局紧急全平</button>
<span class="toolbar-spacer"></span>
<span id="monitor-updated" class="toolbar-meta"></span>
</div>
</details>
<div id="monitor-grid" class="grid-monitor"></div>
</div>
@@ -251,42 +257,48 @@
<div class="page-head">
<h1><span class="head-tag">MKT</span> 行情区</h1>
</div>
<div class="market-toolbar toolbar">
<label class="market-field">
<span>交易所</span>
<select id="market-exchange"></select>
</label>
<label class="market-field market-field-symbol">
<span>币种</span>
<div class="market-symbol-wrap">
<input id="market-symbol" type="text" value="BTC/USDT" placeholder="BTC/USDT" autocomplete="off" />
<div class="market-scan-tabs" role="tablist" aria-label="成交额与背离筛选">
<button type="button" class="market-scan-tab is-active" data-scan-tab="top20" aria-selected="true">Top20</button>
<button type="button" class="market-scan-tab" data-scan-tab="4h" aria-selected="false">4h背离</button>
<button type="button" class="market-scan-tab" data-scan-tab="1d" aria-selected="false">日线背离</button>
<button type="button" class="market-scan-tab" data-scan-tab="1w" aria-selected="false">周线背离</button>
<details id="market-toolbar-fold" class="hub-m-fold market-toolbar-fold">
<summary class="hub-m-fold-summary">
<span class="hub-m-fold-title">选币 · 周期 · 加载</span>
<span id="market-fold-summary" class="hub-m-fold-meta">BTC/USDT · 1d</span>
</summary>
<div class="market-toolbar toolbar">
<label class="market-field">
<span>交易所</span>
<select id="market-exchange"></select>
</label>
<label class="market-field market-field-symbol">
<span>币种</span>
<div class="market-symbol-wrap">
<input id="market-symbol" type="text" value="BTC/USDT" placeholder="BTC/USDT" autocomplete="off" />
<div class="market-scan-tabs" role="tablist" aria-label="成交额与背离筛选">
<button type="button" class="market-scan-tab is-active" data-scan-tab="top20" aria-selected="true">Top20</button>
<button type="button" class="market-scan-tab" data-scan-tab="4h" aria-selected="false">4h背离</button>
<button type="button" class="market-scan-tab" data-scan-tab="1d" aria-selected="false">日线背离</button>
<button type="button" class="market-scan-tab" data-scan-tab="1w" aria-selected="false">周线背离</button>
</div>
</div>
</div>
</label>
<label class="market-field">
<span>周期</span>
<select id="market-timeframe">
<option value="1m">1m</option>
<option value="5m">5m</option>
<option value="15m">15m</option>
<option value="1h">1h</option>
<option value="2h">2h</option>
<option value="4h">4h</option>
<option value="1d" selected>1d</option>
<option value="1w">1w</option>
</select>
</label>
<button type="button" id="market-load" class="primary">加载</button>
<button type="button" id="market-refresh" class="ghost" title="清空该币种本周期 K 线缓存并从交易所重新拉取">清库重拉</button>
<span class="toolbar-spacer"></span>
<span id="market-bar-countdown" class="toolbar-meta market-countdown"></span>
<span id="market-updated" class="toolbar-meta"></span>
</div>
</label>
<label class="market-field">
<span>周期</span>
<select id="market-timeframe">
<option value="1m">1m</option>
<option value="5m">5m</option>
<option value="15m">15m</option>
<option value="1h">1h</option>
<option value="2h">2h</option>
<option value="4h">4h</option>
<option value="1d" selected>1d</option>
<option value="1w">1w</option>
</select>
</label>
<button type="button" id="market-load" class="primary">加载</button>
<button type="button" id="market-refresh" class="ghost" title="清空该币种本周期 K 线缓存并从交易所重新拉取">清库重拉</button>
<span class="toolbar-spacer"></span>
<span id="market-bar-countdown" class="toolbar-meta market-countdown"></span>
<span id="market-updated" class="toolbar-meta"></span>
</div>
</details>
<div id="market-vol-rank-anchor" class="market-vol-rank-anchor">
<div id="market-vol-rank-sheet" class="market-vol-rank-sheet hidden" aria-hidden="true">
<div id="market-vol-rank-meta" class="market-vol-rank-meta">加载中…</div>
@@ -729,8 +741,12 @@
<button type="button" id="calc-btn-refresh" class="primary">刷新</button>
<span id="calc-refresh-status" class="toolbar-meta"></span>
</div>
<div class="calc-layout">
<section class="calc-card card">
<div class="calc-mobile-tabs" role="tablist" aria-label="计算器切换">
<button type="button" class="calc-m-tab is-active" data-calc-tab="trend" role="tab" aria-selected="true">趋势回调</button>
<button type="button" class="calc-m-tab" data-calc-tab="roll" role="tab" aria-selected="false">滚仓</button>
</div>
<div class="calc-layout" data-calc-tab="trend">
<section class="calc-card card" data-calc-pane="trend">
<h2>趋势回调计算器</h2>
<p class="calc-hint">逻辑与实例策略页一致:首仓 50% + 补仓网格;止损金额 = 资金 × 风险%.</p>
<form id="calc-trend-form" class="calc-form">
@@ -793,7 +809,7 @@
<div id="calc-trend-result" class="calc-result hidden"></div>
</section>
<section class="calc-card card">
<section class="calc-card card" data-calc-pane="roll">
<h2>滚仓计算器</h2>
<p class="calc-hint">首仓按「单次风险」以损定仓;每次滚仓后合并持仓打到新止损 ≈ 单次风险;止盈锁定首仓价不变.最多 3 次滚仓.</p>
<form id="calc-roll-form" class="calc-form">
@@ -1280,9 +1296,9 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260706-div-scan-ui"></script>
<script src="/assets/chart.js?v=20260715-fs-landscape"></script>
<script src="/assets/plan.js?v=20260614-plan-refresh"></script>
<script src="/assets/calculator.js?v=3"></script>
<script src="/assets/calculator.js?v=20260715-calc-tabs"></script>
<script src="/assets/trade_stats_calendar.js?v=3"></script>
<script src="/assets/archive.js?v=20260710-archive-cum2"></script>
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
@@ -1295,6 +1311,6 @@
<script src="/assets/options_expiry_countdown.js?v=1"></script>
<script src="/assets/options_position_cards.js?v=1"></script>
<script src="/assets/backup.js?v=1"></script>
<script src="/assets/app.js?v=20260715-hub-phone-clean"></script>
<script src="/assets/app.js?v=20260715-hub-phone-folds"></script>
</body>
</html>