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
+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">