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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user