Hub monitor: OKX left column with dual inner cards when options enabled.

Use a stats bar plus left OKX / right Binance+Gate split layout with equal-height inner perpetual and options cards and no in-card scrolling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 13:33:13 +08:00
parent 5e66600727
commit dda022d3c3
3 changed files with 216 additions and 36 deletions
+117
View File
@@ -1164,6 +1164,123 @@ html[data-theme="light"] .host-metric-bar {
grid-template-columns: 1fr;
}
.grid-monitor.grid-monitor-options-split {
display: flex;
flex-direction: column;
gap: 16px;
}
.grid-monitor.grid-monitor-options-split .monitor-stats-card {
width: 100%;
}
.grid-monitor.grid-monitor-options-split .monitor-stats-grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.monitor-split-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
align-items: stretch;
min-height: 0;
}
.monitor-split-left,
.monitor-split-right {
min-width: 0;
min-height: 0;
}
.monitor-split-left > .card,
.monitor-split-right > .card {
height: 100%;
}
.monitor-split-right {
display: grid;
grid-template-rows: 1fr 1fr;
gap: 16px;
}
.card-monitor-okx-split,
.card-monitor-split-side {
display: flex;
flex-direction: column;
overflow: visible;
}
.card-monitor-okx-split .card-body,
.card-monitor-split-side .card-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
min-height: 0;
}
.hub-inner-cards {
flex: 1;
display: grid;
grid-template-rows: 1fr 1fr;
gap: 12px;
min-height: 0;
}
.hub-inner-card {
display: flex;
flex-direction: column;
min-height: 0;
border: 1px solid var(--border-soft);
border-radius: 10px;
background: color-mix(in srgb, var(--inset-surface) 92%, transparent);
overflow: visible;
}
.hub-inner-card-head {
flex: 0 0 auto;
padding: 8px 12px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--text);
border-bottom: 1px solid var(--border-soft);
}
.hub-inner-card-options .hub-inner-card-head {
color: var(--accent);
}
.hub-inner-card-body {
flex: 1;
padding: 10px 12px;
overflow: visible;
min-height: 0;
}
.grid-monitor-options-split .hub-options-table-wrap,
.grid-monitor-options-split .pos-table-wrap,
.grid-monitor-options-split .table-scroll {
overflow: visible;
max-height: none;
}
@media (max-width: 1100px) {
.grid-monitor.grid-monitor-options-split .monitor-stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 960px) {
.monitor-split-body {
grid-template-columns: 1fr;
}
.monitor-split-right {
grid-template-rows: auto auto;
}
}
.monitor-stats-card .card-head {
padding-bottom: 0;
}
+97 -34
View File
@@ -120,6 +120,7 @@
}
let tpslPending = null;
let lastMonitorRows = [];
let monitorGridOptionsSplit = false;
let lastMonitorTotals = null;
let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1";
@@ -1632,10 +1633,14 @@
el.innerHTML = `<span class="mas-item mas-ok">正常 ${ok}</span><span class="mas-sep">·</span><span class="mas-item mas-warn">关注 ${warn}</span><span class="mas-sep">·</span><span class="mas-item mas-err">异常 ${err}</span>`;
}
/** 监控卡片列数:桌面 2×2(统计+三所);手机 2 列瓦片 */
/** 监控卡片列数:桌面 2×2(统计+三所);期权分栏时由 CSS 控制 */
function syncMonitorGridColumns(gridEl, itemCount, opts) {
if (!gridEl) return;
const options = opts || {};
if (options.optionsSplit) {
gridEl.style.gridTemplateColumns = "";
return;
}
if (isMobileLayout()) {
gridEl.style.gridTemplateColumns = options.statsFirst
? "1fr"
@@ -1837,8 +1842,10 @@
wasMobile = nowMobile;
const box = document.getElementById("monitor-grid");
if (box && lastMonitorRows.length) {
const split = monitorOptionsSplitActive(lastMonitorRows);
syncMonitorGridColumns(box, lastMonitorRows.length + (lastMonitorTotals ? 1 : 0), {
statsFirst: !!lastMonitorTotals,
statsFirst: !!lastMonitorTotals && !split,
optionsSplit: split,
});
updateMonitorAlertSummary(lastMonitorRows);
}
@@ -2200,16 +2207,32 @@
}
const mobileTiles = isMobileLayout() && !expandedExchangeId;
const displayRows = mobileTiles ? sortRowsForMobileDashboard(rows) : rows;
const optionsSplit = monitorOptionsSplitActive(displayRows);
monitorGridOptionsSplit = optionsSplit;
const showStatsCard = !expandedExchangeId;
box.classList.toggle("grid-monitor-tiles", mobileTiles);
box.classList.toggle("grid-monitor-2x2", showStatsCard && !mobileTiles);
box.classList.toggle("grid-monitor-2x2", showStatsCard && !mobileTiles && !optionsSplit);
box.classList.toggle("grid-monitor-with-stats", showStatsCard && mobileTiles);
box.classList.toggle("grid-monitor-options-split", optionsSplit && showStatsCard);
try {
const statsHtml = showStatsCard ? renderMonitorStatsCard(lastMonitorTotals) : "";
const cardsHtml =
displayRows
.map((r) => (mobileTiles ? renderMonitorTile(r) : renderMonitorCard(r)))
.join("") || (showStatsCard ? "" : '<div class="err">无已启用账户</div>');
let cardsHtml = "";
if (optionsSplit) {
const okxRow = displayRows.find((r) => rowHasOptionsLayout(r));
const otherRows = displayRows.filter((r) => !rowHasOptionsLayout(r));
const leftHtml = okxRow ? renderMonitorCard(okxRow, { okxSplit: true }) : "";
const rightHtml = otherRows.map((r) => renderMonitorCard(r, { splitSide: true })).join("");
cardsHtml =
`<div class="monitor-split-body">` +
`<div class="monitor-split-left">${leftHtml}</div>` +
`<div class="monitor-split-right">${rightHtml}</div>` +
`</div>`;
} else {
cardsHtml =
displayRows
.map((r) => (mobileTiles ? renderMonitorTile(r) : renderMonitorCard(r)))
.join("") || (showStatsCard ? "" : '<div class="err">无已启用账户</div>');
}
box.innerHTML = statsHtml + cardsHtml;
if (showStatsCard && !cardsHtml && !statsHtml) {
box.innerHTML = '<div class="err">无已启用账户</div>';
@@ -2219,7 +2242,8 @@
box.innerHTML = `<div class="err">监控区渲染失败:${esc(String(err && err.message ? err.message : err))}</div>`;
}
syncMonitorGridColumns(box, displayRows.length + (showStatsCard ? 1 : 0), {
statsFirst: showStatsCard,
statsFirst: showStatsCard && !optionsSplit,
optionsSplit: optionsSplit && showStatsCard,
});
bindMonitorInteractions(box);
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
@@ -3468,33 +3492,59 @@
return html;
}
function renderOptionsMonitorSection(row) {
if (!rowHasOptionsLayout(row)) return "";
function monitorOptionsSplitActive(rows) {
if (isMobileLayout() || expandedExchangeId) return false;
return (rows || []).some((r) => rowHasOptionsLayout(r));
}
function renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday) {
let html = '<div class="hub-inner-card hub-inner-card-perp">';
html += '<div class="hub-inner-card-head">永续账户</div>';
html += '<div class="hub-inner-card-body">';
html += renderAccountStatRow(row, ag);
html += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
html += "</div></div>";
return html;
}
function renderOptionsSectionBody(row) {
const opt = row.options || {};
let html = '<div class="hub-monitor-block hub-monitor-options">';
html += '<div class="hub-monitor-block-label">期权账户</div>';
let html = "";
if (opt.enabled === false) {
html += renderStatRow(null, null, null);
html += '<div class="section-title hub-options-title">期权持仓</div>';
html += '<div class="empty-hint">期权未启用(OKX_OPTIONS_ENABLED</div>';
html += "</div>";
return html;
}
if (opt.ok === false) {
} else if (opt.ok === false) {
html += renderStatRow(null, null, null);
html += '<div class="section-title hub-options-title">期权持仓</div>';
html += `<div class="err">${esc(opt.msg || "期权数据不可用")}</div>`;
html += "</div>";
return html;
}
const pos = Array.isArray(opt.positions) ? opt.positions : [];
const bal = optionsBalanceFields(opt);
html += renderStatRow(bal.funding, bal.trading, bal.upl);
html += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
html += renderOptionsPositionsTable(pos);
if (row.flask_url_browser || row.flask_url) {
html += `<div class="hub-options-actions"><a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">打开期权页</a></div>`;
} else {
const pos = Array.isArray(opt.positions) ? opt.positions : [];
const bal = optionsBalanceFields(opt);
html += renderStatRow(bal.funding, bal.trading, bal.upl);
html += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
html += renderOptionsPositionsTable(pos);
if (row.flask_url_browser || row.flask_url) {
html += `<div class="hub-options-actions"><a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">打开期权页</a></div>`;
}
}
return html;
}
function renderOptionsInnerCard(row) {
let html = '<div class="hub-inner-card hub-inner-card-options">';
html += '<div class="hub-inner-card-head">期权账户</div>';
html += '<div class="hub-inner-card-body">';
html += renderOptionsSectionBody(row);
html += "</div></div>";
return html;
}
function renderOptionsMonitorSection(row) {
if (!rowHasOptionsLayout(row)) return "";
let html = '<div class="hub-monitor-block hub-monitor-options">';
html += '<div class="hub-monitor-block-label">期权账户</div>';
html += renderOptionsSectionBody(row);
html += "</div>";
return html;
}
@@ -3523,12 +3573,19 @@
const intraday = isIntradayDisciplineRow(row);
let inner = "";
if (rowHasOptionsLayout(row)) {
inner += '<div class="hub-monitor-block hub-monitor-perp">';
inner += '<div class="hub-monitor-block-label">永续账户</div>';
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
inner += "</div>";
inner += renderOptionsMonitorSection(row);
if (monitorGridOptionsSplit) {
inner += '<div class="hub-inner-cards">';
inner += renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday);
inner += renderOptionsInnerCard(row);
inner += "</div>";
} else {
inner += '<div class="hub-monitor-block hub-monitor-perp">';
inner += '<div class="hub-monitor-block-label">永续账户</div>';
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
inner += "</div>";
inner += renderOptionsMonitorSection(row);
}
} else {
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
@@ -3878,7 +3935,8 @@
</div>`;
}
function renderMonitorCard(row) {
function renderMonitorCard(row, layoutOpts) {
const opts = layoutOpts || {};
const ag = row.agent || {};
const pos = Array.isArray(ag.positions) ? ag.positions : [];
const hm = row.hub_monitor || {};
@@ -3924,7 +3982,12 @@
: "";
const intraday = isIntradayDisciplineRow(row);
const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : "";
return `<div class="card ${cardCls}" data-ex-id="${esc(row.id)}">
const layoutCls = opts.okxSplit
? " card-monitor-okx-split"
: opts.splitSide
? " card-monitor-split-side"
: "";
return `<div class="card ${cardCls}${layoutCls}" data-ex-id="${esc(row.id)}">
<div class="card-head card-expand-zone" title="点击放大全屏">
<div>
<div class="card-title-row">
+2 -2
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=20260707-hub-options-split" />
<link rel="stylesheet" href="/assets/app.css?v=20260707-monitor-options-split" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=3" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -1151,6 +1151,6 @@
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=3"></script>
<script src="/assets/backup.js?v=1"></script>
<script src="/assets/app.js?v=20260707-hub-options-split"></script>
<script src="/assets/app.js?v=20260707-monitor-options-split"></script>
</body>
</html>