Split OKX monitor into perp and options cards.

Align four exchange cards in a 2x2 grid so 1080p layout stays balanced.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-16 13:47:21 +08:00
parent bde8c517e0
commit 018b4d5601
4 changed files with 98 additions and 52 deletions
+26
View File
@@ -4,6 +4,32 @@
---
## 2026-07-16 · OKX 拆成永续/期权双卡(2×2 对齐)
### 修改原因
OKX 单卡内嵌永续+期权过高,右侧币安/Gate 两卡对不齐,1080p 观感不协调。按产品建议改为四卡对齐。
### 修改的地方
| 文件 | 改动摘要 |
|------|----------|
| `manual_trading_hub/static/app.js` | 期权分栏时 OKX 渲染为「·永续」「·期权」两张独立卡;标题与操作按钮按卡片分流 |
| `manual_trading_hub/static/app.css` | 左右列均 `1fr/1fr`,四卡 2×2 等高对齐;卡体内滚 |
| `manual_trading_hub/static/index.html` | 静态资源缓存 `20260716-hub-okx-2x2` |
### 达成的目标
监控区呈现:左上永续 / 左下期权 / 右上币安 / 右下 Gate,四卡对齐。
### 交付之后的验收
1. 桌面监控区可见 `OKX_趋势 · 永续``OKX_趋势 · 期权` 两张独立卡。
2. 四卡与右侧币安、Gate 同行等高,不再一大两小。
3. 点击任一张 OKX 卡标题仍可全屏;期权卡「全平」不重复出现。
---
## 2026-07-16 · 1080p 监控区无持仓空洞收紧
### 修改原因
+29 -35
View File
@@ -1275,31 +1275,25 @@ html[data-theme="light"] .host-metric-bar {
.monitor-split-right {
min-width: 0;
min-height: 0;
}
.monitor-split-left > .card {
height: 100%;
}
.monitor-split-right {
display: grid;
/* 无持仓时勿 1fr 均分拉高,按内容收紧;有仓时自然增高 */
grid-template-rows: auto auto;
align-content: start;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
overflow: auto;
align-content: stretch;
overflow: hidden;
}
.monitor-split-left > .card,
.monitor-split-right > .card {
height: auto;
height: 100%;
min-height: 0;
overflow: hidden;
}
.card-monitor-okx-split,
.card-monitor-split-side {
display: flex;
flex-direction: column;
overflow: visible;
overflow: hidden;
}
.card-monitor-okx-split .card-body,
@@ -1307,7 +1301,7 @@ html[data-theme="light"] .host-metric-bar {
flex: 1;
display: flex;
flex-direction: column;
overflow: visible;
overflow: auto;
min-height: 0;
}
@@ -1374,6 +1368,7 @@ html[data-theme="light"] .host-metric-bar {
grid-template-columns: 1fr;
}
.monitor-split-left,
.monitor-split-right {
grid-template-rows: auto auto;
}
@@ -9898,15 +9893,18 @@ html[data-theme="light"] .hub-logs-card-hint {
height: auto;
}
body.hub-page-monitor:not(.hub-phone) .monitor-split-left,
body.hub-page-monitor:not(.hub-phone) .monitor-split-right {
gap: 8px;
grid-template-rows: auto auto;
align-content: start;
overflow: auto;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
align-content: stretch;
overflow: hidden;
}
body.hub-page-monitor:not(.hub-phone) .monitor-split-left > .card,
body.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card {
height: auto;
height: 100%;
min-height: 0;
}
body.hub-page-monitor:not(.hub-phone) .card-monitor-okx-split,
@@ -9922,7 +9920,7 @@ html[data-theme="light"] .hub-logs-card-hint {
body.hub-page-monitor:not(.hub-phone) .card-monitor-okx-split .card-body,
body.hub-page-monitor:not(.hub-phone) .card-monitor-split-side .card-body {
padding: 8px 10px;
overflow: hidden;
overflow: auto;
}
body.hub-page-monitor:not(.hub-phone) .hub-inner-cards {
@@ -10208,38 +10206,34 @@ body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .mo
align-items: stretch;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-left > .card {
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-left > .card,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card {
height: 100%;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card {
height: auto;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-left,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right {
grid-template-rows: auto auto;
align-content: start;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
align-content: stretch;
gap: 6px;
min-height: 0;
overflow: auto;
overflow: hidden;
}
/* 竖屏(<960)叠成 OKX 上 + Gate/币安并排 */
/* 竖屏(<960)叠成 OKX 两卡在上 + Gate/币安并排 */
@media (max-width: 960px) {
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-body {
grid-template-rows: minmax(0, 1.35fr) minmax(0, 1fr);
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: auto;
align-content: stretch;
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-left {
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card {
height: 100%;
body.hub-tablet.hub-page-monitor:not(.hub-phone) .monitor-split-right {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
}
+41 -15
View File
@@ -2434,7 +2434,11 @@
if (optionsSplit) {
const okxRow = displayRows.find((r) => rowHasOptionsLayout(r));
const otherRows = displayRows.filter((r) => !rowHasOptionsLayout(r));
const leftHtml = okxRow ? renderMonitorCard(okxRow, { okxSplit: true }) : "";
/* OKX 拆成永续+期权两张独立卡,与右侧两所组成 2×2 对齐 */
const leftHtml = okxRow
? renderMonitorCard(okxRow, { okxPart: "perp", splitSide: true }) +
renderMonitorCard(okxRow, { okxPart: "options", splitSide: true })
: "";
const rightHtml = otherRows.map((r) => renderMonitorCard(r, { splitSide: true })).join("");
cardsHtml =
`<div class="monitor-split-body">` +
@@ -3852,12 +3856,26 @@
return html;
}
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap, layoutOpts) {
const layout = layoutOpts || {};
const tickMap = buildPriceTickMap(row);
const intraday = isIntradayDisciplineRow(row);
let inner = "";
if (layout.okxPart === "perp") {
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
inner += renderCardStrategyStats(row, hm, flaskOk);
inner += `<div class="card-expand-hint">点击标题栏进入全屏</div>`;
return inner;
}
if (layout.okxPart === "options") {
inner += renderOptionsSectionBody(row);
inner += `<div class="card-expand-hint">点击标题栏进入全屏 · 期权专页可下单</div>`;
return inner;
}
if (rowHasOptionsLayout(row)) {
if (monitorGridOptionsSplit) {
/* 兼容旧单卡内嵌;正常分栏已走 okxPart 双卡 */
inner += '<div class="hub-inner-cards">';
inner += renderPerpetualInnerCard(row, ag, pos, orders, trends, tickMap, intraday);
inner += renderOptionsInnerCard(row);
@@ -4266,41 +4284,49 @@
inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
} else {
inner = renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap);
inner = renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap, opts);
}
const online = row.http_ok && agOk;
const cardCls = online ? "card-online" : "card-offline";
const dotCls = online ? "ok" : "bad";
const flaskOpen = row.flask_url_browser || row.flask_url;
const okxPart = opts.okxPart || "";
const isOkxOptionsCard = okxPart === "options";
const isOkxPerpCard = okxPart === "perp";
const titleSuffix = isOkxPerpCard ? " · 永续" : isOkxOptionsCard ? " · 期权" : "";
const openFlask = flaskOpen
? `<a class="btn-link btn-open-instance btn-open-trade" href="#" data-ex-id="${esc(row.id)}" data-next="/trade" data-new-tab="1">打开实例</a>`
: "";
const openTrade = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/trade">下单</a>`
: "";
const openKey = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">监控位</a>`
: "";
const openTrade =
flaskOpen && !isOkxOptionsCard
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/trade">下单</a>`
: "";
const openKey =
flaskOpen && !isOkxOptionsCard
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">监控位</a>`
: "";
const openReview = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>`
: "";
const openOptions =
flaskOpen && (row.capabilities || []).includes("options")
flaskOpen && ((row.capabilities || []).includes("options") || isOkxOptionsCard)
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/options">期权</a>`
: "";
const intraday = isIntradayDisciplineRow(row);
const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : "";
const fcHeadBadge = intraday && !isOkxOptionsCard ? forceCloseHeadBadgeHtml(row.force_close) : "";
const showCloseAll = !intraday && !isOkxOptionsCard;
const layoutCls = opts.okxSplit
? " card-monitor-okx-split"
: opts.splitSide
: opts.splitSide || okxPart
? " card-monitor-split-side"
: "";
return `<div class="card ${cardCls}${layoutCls}" data-ex-id="${esc(row.id)}">
const partAttr = okxPart ? ` data-okx-part="${esc(okxPart)}"` : "";
return `<div class="card ${cardCls}${layoutCls}" data-ex-id="${esc(row.id)}"${partAttr}>
<div class="card-head card-expand-zone" title="点击放大全屏">
<div>
<div class="card-title-row">
<span class="status-dot ${dotCls}" title="${online ? "在线" : "离线"}"></span>
<div class="card-title"><span>${esc(row.name)}</span>${fcHeadBadge}${formatRiskStatusBadge(hm.risk_status)}</div>
<div class="card-title"><span>${esc(row.name)}${esc(titleSuffix)}</span>${fcHeadBadge}${formatRiskStatusBadge(hm.risk_status)}</div>
</div>
<div class="card-sub">${esc(flaskOpen || "")}</div>
</div>
@@ -4310,7 +4336,7 @@
${openKey}
${openOptions}
${openReview}
${intraday ? "" : `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>`}
${showCloseAll ? `<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>` : ""}
</div>
</div>
<div class="card-body">${inner}</div>
+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=20260716-hub-1080-tight" />
<link rel="stylesheet" href="/assets/app.css?v=20260716-hub-okx-2x2" />
<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>
@@ -1327,6 +1327,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=20260716-hub-desktop-clear"></script>
<script src="/assets/app.js?v=20260716-hub-okx-2x2"></script>
</body>
</html>