Refine monitor 2x2 slots and drop tablet CSS.

Add options target column, reserve pos/monitor rows, equal columns, and remove hub-tablet densify path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-16 14:03:59 +08:00
parent 018b4d5601
commit 0540b585c7
4 changed files with 10288 additions and 10599 deletions
+27
View File
@@ -4,6 +4,33 @@
---
## 2026-07-16 · 监控区 2×2 细化(目标监控列/预留行/去平板专属)
### 修改原因
四卡对齐后需:左右等宽;期权表改目标监控列;去掉打开期权页与永续卡「期权 N仓」;合约卡预留仓位+监控位两行并统一全屏提示;多仓时同行左右一起长高;平板改用浏览器 80% 缩放,去掉 hub-tablet 专属样式(手机 UI 不动)。
### 修改的地方
| 文件 | 改动摘要 |
|------|----------|
| `manual_trading_hub/static/app.js` | 平铺 `monitor-split-2x2`;期权表删指数/到期平衡/平掉回本,加目标监控列(有=绿/无=`—`);去打开期权页;永续卡隐藏期权徽章;预留仓位/监控位槽;去掉 `isTabletLayout` |
| `manual_trading_hub/static/app.css` | 左右 1:1`minmax(min-content,1fr)` 同行同高可外扩;删除全部 `hub-tablet` 规则 |
| `manual_trading_hub/static/index.html` | 缓存 `20260716-hub-2x2-slots` |
### 达成的目标
桌面监控区四卡等宽对齐;期权看目标监控列;永续卡结构为仓位行+监控位行+提示行;≥2 仓左右一起加高;平板不再走单独 CSS。
### 交付之后的验收
1. 左右列等宽;永续卡无「期权 1仓」、无「打开期权页」。
2. 期权表有「目标监控」列,有监控绿色、无则 `—`;无指数/到期平衡/平掉回本。
3. 永续/币安卡可见预留仓位行、监控位行与完整全屏提示文案。
4. 多开仓后该行变高且左右同高;`body``hub-tablet`;手机布局未改。
---
## 2026-07-16 · OKX 拆成永续/期权双卡(2×2 对齐)
### 修改原因
+58 -436
View File
@@ -1271,6 +1271,30 @@ html[data-theme="light"] .host-metric-bar {
min-height: 0;
}
/* 四卡平铺 2×2:左右等宽;同行同高;多仓时该行 min-content 变高,左右一起长 */
.monitor-split-body.monitor-split-2x2 {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: minmax(min-content, 1fr) minmax(min-content, 1fr);
gap: 12px;
flex: 1 1 auto;
min-height: 0;
}
.monitor-split-2x2 > .card {
min-width: 0;
min-height: 0;
height: 100%;
overflow: hidden;
}
.monitor-split-2x2 > .card-monitor-placeholder {
visibility: hidden;
pointer-events: none;
border: none;
background: transparent;
box-shadow: none;
}
.monitor-split-left,
.monitor-split-right {
min-width: 0;
@@ -1305,10 +1329,27 @@ html[data-theme="light"] .host-metric-bar {
min-height: 0;
}
.hub-slot-pos,
.hub-slot-monitor {
min-height: 2.4em;
flex: 0 0 auto;
}
.hub-slot-pos[data-pos-count="0"] .empty-hint,
.hub-slot-monitor[data-key-count="0"] .empty-hint {
margin: 0;
padding: 6px 0;
}
.hub-opt-target-cell.is-on {
color: var(--green);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.hub-inner-cards {
flex: 1;
display: grid;
/* 永续按内容高度,期权吃剩余 —— 避免「无持仓」半屏空洞 */
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
min-height: 0;
@@ -1364,8 +1405,10 @@ html[data-theme="light"] .host-metric-bar {
}
@media (max-width: 960px) {
.monitor-split-body {
.monitor-split-body,
.monitor-split-body.monitor-split-2x2 {
grid-template-columns: 1fr;
grid-template-rows: none;
}
.monitor-split-left,
@@ -9789,7 +9832,7 @@ html[data-theme="light"] .hub-logs-card-hint {
}
}
/* ── 一屏适配:桌面 1080p + 平板 CSS 视口(2560×1600@2x≈1280×800) ── */
/* ── 一屏适配:桌面(含用户自行缩放的平板视口);手机端 .hub-phone 另走 ── */
@media (min-width: 721px) and (min-height: 650px) {
body.hub-page-monitor:not(.hub-phone) .app-shell,
body.hub-page-market:not(.hub-phone) .app-shell,
@@ -9801,7 +9844,8 @@ html[data-theme="light"] .hub-logs-card-hint {
padding-bottom: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
}
body.hub-page-monitor:not(.hub-phone) .app-header,
@@ -9818,7 +9862,7 @@ html[data-theme="light"] .hub-logs-card-hint {
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow: visible;
}
body.hub-page-monitor:not(.hub-phone) #page-monitor > .page-head {
@@ -9858,7 +9902,7 @@ html[data-theme="light"] .hub-logs-card-hint {
flex: 1 1 auto;
min-height: 0;
gap: 8px;
overflow: hidden;
overflow: visible;
}
body.hub-page-monitor:not(.hub-phone) .grid-monitor.grid-monitor-options-split {
@@ -9893,6 +9937,12 @@ html[data-theme="light"] .hub-logs-card-hint {
height: auto;
}
body.hub-page-monitor:not(.hub-phone) .monitor-split-body.monitor-split-2x2 {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: minmax(min-content, 1fr) minmax(min-content, 1fr);
gap: 8px;
}
body.hub-page-monitor:not(.hub-phone) .monitor-split-left,
body.hub-page-monitor:not(.hub-phone) .monitor-split-right {
gap: 8px;
@@ -9902,7 +9952,8 @@ html[data-theme="light"] .hub-logs-card-hint {
}
body.hub-page-monitor:not(.hub-phone) .monitor-split-left > .card,
body.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card {
body.hub-page-monitor:not(.hub-phone) .monitor-split-right > .card,
body.hub-page-monitor:not(.hub-phone) .monitor-split-2x2 > .card {
height: 100%;
min-height: 0;
}
@@ -9971,13 +10022,6 @@ html[data-theme="light"] .hub-logs-card-hint {
font-size: 11px;
}
/* 1080p 短屏:左侧略宽,把横向空间给 OKX 期权表 */
@media (max-height: 1100px) {
body.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-split-body {
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
}
}
/* —— 行情区 —— */
body.hub-page-market:not(.hub-phone) #page-market {
flex: 1 1 auto;
@@ -10109,425 +10153,3 @@ html[data-theme="light"] .hub-logs-card-hint {
gap: 6px;
}
}
/*
* 平板(hub-tablet):一屏 + 提密度 + 不裁切正文。
* 典型 CSS 视口 ~1280×800 横 / ~800×1280 竖。
* 外壳锁 100dvh;字号/间距压缩让常规内容装下;
* 持仓很多时仅表体内滚,不把卡片文字拦腰切掉。
*/
body.hub-tablet:not(.hub-phone).hub-page-monitor .app-shell,
body.hub-tablet:not(.hub-phone).hub-page-funds .app-shell,
body.hub-tablet:not(.hub-phone).hub-page-market .app-shell {
height: 100dvh;
max-height: 100dvh;
overflow: hidden;
padding-top: 0;
padding-bottom: max(6px, env(safe-area-inset-bottom));
padding-left: max(10px, env(safe-area-inset-left));
padding-right: max(10px, env(safe-area-inset-right));
}
body.hub-tablet:not(.hub-phone).hub-page-monitor .app-header,
body.hub-tablet:not(.hub-phone).hub-page-funds .app-header,
body.hub-tablet:not(.hub-phone).hub-page-market .app-header {
padding: 6px 0 4px;
gap: 8px;
}
body.hub-tablet:not(.hub-phone) .brand-sub {
display: none;
}
body.hub-tablet:not(.hub-phone) .brand-title {
font-size: 13px;
}
body.hub-tablet:not(.hub-phone) .top-nav a {
font-size: 11px;
padding: 4px 7px;
}
/* —— 监控区:一屏 flex + 密度 —— */
body.hub-tablet.hub-page-monitor:not(.hub-phone) #page-monitor {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) #page-monitor > .page-head {
margin: 4px 0 2px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) #page-monitor > .page-head h1 {
font-size: 14px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) #page-monitor .page-desc {
display: none;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) #page-monitor .grid-monitor {
flex: 1 1 auto;
min-height: 0;
gap: 6px;
overflow: hidden;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor.grid-monitor-options-split {
height: 100%;
gap: 6px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-stats-card {
padding: 4px 8px;
gap: 8px 10px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-stat-label {
font-size: 9px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-stat-value {
font-size: 12px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-stat-sub {
font-size: 9px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .monitor-split-body {
flex: 1 1 auto;
min-height: 0;
height: auto;
gap: 6px;
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-right > .card {
height: 100%;
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: minmax(0, 1fr) minmax(0, 1fr);
align-content: stretch;
gap: 6px;
min-height: 0;
overflow: hidden;
}
/* 竖屏(<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-left {
grid-template-rows: minmax(0, 1fr) 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: minmax(0, 1fr);
}
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-okx-split,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-split-side {
overflow: hidden;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-okx-split .card-head,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-split-side .card-head {
padding: 5px 8px;
gap: 6px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-title {
font-size: 11px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-sub {
font-size: 9px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-actions {
gap: 4px;
flex-wrap: wrap;
max-width: 46%;
justify-content: flex-end;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-actions .btn-link,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-actions button {
font-size: 10px;
padding: 3px 6px;
min-height: 0;
line-height: 1.2;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-okx-split .card-body,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .card-monitor-split-side .card-body {
padding: 6px 8px;
overflow: hidden;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .hub-inner-cards {
grid-template-rows: auto minmax(0, 1fr);
gap: 5px;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .hub-inner-card {
overflow: hidden;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .hub-inner-card-head {
padding: 4px 6px;
font-size: 10px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .hub-inner-card-body {
padding: 5px 6px;
overflow: auto;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .stat-row {
gap: 6px;
margin-bottom: 6px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .stat-box {
padding: 5px 7px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .stat-label {
font-size: 9px;
margin-bottom: 2px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .stat-value {
font-size: 13px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .stat-value small {
font-size: 10px !important;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .section-title {
margin: 4px 0 3px;
padding-bottom: 3px;
font-size: 10px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .empty-hint {
padding: 4px 0;
font-size: 10px;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .hub-options-table-wrap,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .pos-table-wrap,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .grid-monitor-options-split .table-scroll {
overflow: auto;
max-height: 100%;
min-height: 0;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .data-table th,
body.hub-tablet.hub-page-monitor:not(.hub-phone) .data-table td {
padding: 3px 4px;
font-size: 10px;
line-height: 1.25;
}
body.hub-tablet.hub-page-monitor:not(.hub-phone) .data-table th {
white-space: nowrap;
}
/* —— 资金概况:一屏 + 曲线让位给分户卡片 —— */
body.hub-tablet.hub-page-funds:not(.hub-phone) #page-funds,
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stage,
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stage-inner {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stage-inner {
padding: 6px 8px 8px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-head {
margin-bottom: 2px !important;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-head h1 {
font-size: 14px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-desc-fold {
display: none;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-toolbar {
margin-bottom: 4px;
gap: 6px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-pnl-banner {
padding: 6px 8px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-pnl-value {
font-size: 1.05rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-summary {
margin-bottom: 4px;
gap: 6px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stat-card {
padding: 6px 8px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stat-label {
font-size: 0.55rem;
margin-bottom: 2px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stat-value {
font-size: 0.98rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stat-card-primary .funds-stat-value {
font-size: 1.08rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-stat-sub {
font-size: 0.62rem;
margin-top: 2px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-meta {
margin: 0 0 4px;
padding: 3px 6px;
font-size: 0.68rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-chart-panel {
flex: 1 1 auto;
min-height: 90px;
max-height: 26vh;
margin-bottom: 6px;
display: flex;
flex-direction: column;
overflow: hidden;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-chart-head {
padding: 4px 8px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-chart-tag {
font-size: 0.58rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-chart-host {
flex: 1 1 auto;
height: auto;
min-height: 70px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-section-head {
margin-bottom: 4px;
font-size: 11px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-accounts {
flex: 0 1 auto;
min-height: 0;
max-height: 34vh;
overflow: auto;
gap: 6px;
padding: 0;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-card {
padding: 7px 9px;
gap: 5px;
overflow: visible;
min-width: 0;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-name {
font-size: 0.72rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-badge {
font-size: 0.58rem;
padding: 1px 6px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-total {
padding: 5px 7px;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-total .k {
font-size: 0.62rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-total .v {
font-size: 0.92rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-stats {
gap: 4px 8px;
font-size: 0.68rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-stats .k {
font-size: 0.58rem;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-ac-foot {
padding-top: 5px;
font-size: 0.62rem;
}
@media (max-width: 960px) {
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-chart-panel {
max-height: 22vh;
}
body.hub-tablet.hub-page-funds:not(.hub-phone) .funds-accounts {
max-height: 38vh;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* —— 行情:一屏图表 —— */
body.hub-tablet.hub-page-market:not(.hub-phone) #page-market > .page-head h1 {
font-size: 14px;
}
body.hub-tablet.hub-page-market:not(.hub-phone) #page-market .market-ohlcv-bar {
padding: 5px 8px;
}
+104 -64
View File
@@ -72,30 +72,14 @@
const HUB_PHONE_PRIMARY = { monitor: 1, market: 1, calculator: 1, ai: 1 };
/** 窄屏布局:仅按视口宽度,监控区/行情等共用 */
/** 窄屏布局:仅按视口宽度,监控区/行情等共用;手机端 UI 不改 */
function isMobileLayout() {
return window.matchMedia("(max-width: 720px)").matches;
}
/**
* 平板 CSS 视口( 2560×1600@2x1280×800, 竖屏800×1280)
* 禁止仅按高度判断:1080p 电脑开任务栏/浏览器栏时 innerHeight 920,
* 会误加 hub-tablet,字号被压到 ~10px,看起来发糊
*/
function isTabletLayout() {
if (isMobileLayout()) return false;
const w = window.innerWidth || 0;
const h = window.innerHeight || 0;
// 横屏平板:中等宽度 + 偏矮
if (w >= 721 && w <= 1366 && h > 0 && h <= 900) return true;
// 竖屏平板:偏窄 + 偏高
if (w >= 721 && w <= 920 && h >= 900) return true;
return false;
}
function syncHubPhoneShellClass() {
document.body.classList.toggle("hub-phone", isMobileLayout());
document.body.classList.toggle("hub-tablet", isTabletLayout());
document.body.classList.remove("hub-tablet");
}
function closeHubMobileMore() {
@@ -2434,17 +2418,19 @@
if (optionsSplit) {
const okxRow = displayRows.find((r) => rowHasOptionsLayout(r));
const otherRows = displayRows.filter((r) => !rowHasOptionsLayout(r));
/* 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">` +
`<div class="monitor-split-left">${leftHtml}</div>` +
`<div class="monitor-split-right">${rightHtml}</div>` +
`</div>`;
const ph =
'<div class="card card-monitor-split-side card-monitor-placeholder" aria-hidden="true"></div>';
/* 平铺 2×2 顺序:永续|币安 / 期权|Gate —— 同行左右同高,多仓时该行一起长高 */
const cells = [
okxRow ? renderMonitorCard(okxRow, { okxPart: "perp", splitSide: true }) : ph,
otherRows[0] ? renderMonitorCard(otherRows[0], { splitSide: true }) : ph,
okxRow ? renderMonitorCard(okxRow, { okxPart: "options", splitSide: true }) : ph,
otherRows[1] ? renderMonitorCard(otherRows[1], { splitSide: true }) : ph,
];
for (let i = 2; i < otherRows.length; i++) {
cells.push(renderMonitorCard(otherRows[i], { splitSide: true }));
}
cardsHtml = `<div class="monitor-split-body monitor-split-2x2">${cells.join("")}</div>`;
} else {
cardsHtml =
displayRows
@@ -3609,11 +3595,12 @@
return counts;
}
function renderCardStrategyStats(row, hm, flaskOk) {
function renderCardStrategyStats(row, hm, flaskOk, opts) {
const options = opts || {};
const caps = row.capabilities || [];
const chips = [];
if (flaskOk && hm && typeof hm === "object") {
if (caps.includes("key")) {
if (caps.includes("key") && !options.hideKeyChips) {
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}` });
@@ -3626,8 +3613,8 @@
const rollN = Array.isArray(hm.rolls) ? hm.rolls.length : 0;
if (rollN > 0) chips.push({ kind: "roll", label: `顺势加仓 ${rollN}` });
}
// OKX/已勾选期权能力:磁贴始终展示期权入口态(含 0 仓)
if (caps.includes("options") || rowHasOptionsLayout(row)) {
// 永续独立卡不再显示「期权 N仓」(期权只看期权卡);手机磁贴仍可显示
if (!options.hideOptions && (caps.includes("options") || rowHasOptionsLayout(row))) {
const opt = row.options || {};
if (opt.enabled === false) {
chips.push({ kind: "options", label: "期权 · 未启用" });
@@ -3722,13 +3709,37 @@
return `<span class="opt-expiry-cd" data-opt-exp-ms="${esc(ms)}">—</span>`;
}
function renderOptionsPositionsTable(pos) {
if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
function findOptionsTargetForInst(targets, instId) {
const want = String(instId || "").trim();
if (!want) return null;
const list = Array.isArray(targets) ? targets : [];
return (
list.find((t) => String((t && t.inst_id) || "").trim() === want) ||
list.find((t) => String((t && t.instId) || "").trim() === want) ||
null
);
}
function renderOptionsTargetCell(target) {
if (!target) return "<td>—</td>";
const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
return `<td class="hub-opt-target-cell is-on" title="目标监控">${esc(side)} ${esc(px)}</td>`;
}
function renderOptionsPositionsTable(pos, targets) {
if (!pos.length) return '<div class="empty-hint hub-slot-pos">暂无期权持仓</div>';
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
html += "<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>指数</th><th>到期平衡</th><th>平掉回本</th><th>净盈亏</th><th>收益率</th>";
html +=
"<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>目标监控</th><th>净盈亏</th><th>收益率</th>";
html += "</tr></thead><tbody>";
pos.forEach((p) => {
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
const optType =
(p.opt_type || "").toUpperCase() === "C"
? "Call"
: (p.opt_type || "").toUpperCase() === "P"
? "Put"
: p.opt_type || "—";
const preview = p.close_preview || {};
let net = preview.estimated_pnl;
if (net == null && preview.total_received != null && p.premium_paid != null) {
@@ -3738,14 +3749,13 @@
if (roi == null && net != null && Number(p.premium_paid) > 0) {
roi = (Number(net) / Number(p.premium_paid)) * 100;
}
const target = findOptionsTargetForInst(targets, p.inst_id);
html += `<tr>
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
<td>${esc(optType)}</td>
<td>${esc(p.pos)}</td>
<td>${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
${renderOptionsTargetCell(target)}
<td class="${pnlCls(net)}">${net == null ? "—" : fmt(net, 2)}</td>
<td class="${pnlCls(net)}">${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}</td>
</tr>`;
@@ -3801,20 +3811,11 @@
const targets = Array.isArray(opt.target_monitors) ? opt.target_monitors : [];
const bal = optionsBalanceFields(opt);
html += renderStatRow(bal.funding, bal.trading, bal.upl);
if (targets.length) {
html += `<div class="section-title hub-options-title">目标监控 · ${targets.length}</div>`;
html += '<div class="hub-opt-target-list">';
targets.forEach((t) => {
const side = String(t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
html += `<div class="hub-opt-target-item"><code title="${esc(t.inst_id || "")}">${esc(shortOptionsInst(t.inst_id))}</code> <span>${side} ${esc(fmt(t.target_index, 1))}</span></div>`;
});
html += "</div>";
}
html += `<div class="section-title hub-options-title">期权持仓 · ${pos.length} 仓</div>`;
html += layout === "cards" ? renderOptionsPositionsCards(pos) : 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>`;
}
html +=
layout === "cards"
? renderOptionsPositionsCards(pos)
: renderOptionsPositionsTable(pos, targets);
}
return html;
}
@@ -3838,9 +3839,11 @@
}
function renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday) {
const n = Array.isArray(pos) ? pos.length : 0;
let html = "";
html += `<div class="section-title">交易所持仓 · ${pos.length} 仓</div>`;
if (pos.length) {
html += `<div class="section-title">交易所持仓 · ${n} 仓</div>`;
html += `<div class="hub-slot-pos" data-pos-count="${n}">`;
if (n) {
html += renderGridPositionsTable(
row.id,
row.key || row.id,
@@ -3853,29 +3856,66 @@
} else {
html += '<div class="empty-hint">无持仓</div>';
}
html += "</div>";
return html;
}
function renderPerpetualKeyMonitorSlot(keys, kmap) {
const list = Array.isArray(keys) ? keys : [];
let html = `<div class="section-title">监控位 · ${list.length}</div>`;
html += `<div class="hub-slot-monitor" data-key-count="${list.length}">`;
if (list.length) {
const first = list.slice(0, 1);
html += renderKeySection(first, kmap || {});
if (list.length > 1) {
html += `<div class="empty-hint hub-slot-more">另有 ${list.length - 1} 个监控位 · 点标题全屏查看</div>`;
}
} else {
html += '<div class="empty-hint">无监控位</div>';
}
html += "</div>";
return html;
}
const HUB_EXPAND_HINT =
"点击标题栏进入全屏 · 委托 / 关键位 / 下单监控 / 趋势回调 / 顺势加仓";
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap, layoutOpts) {
const layout = layoutOpts || {};
const tickMap = buildPriceTickMap(row);
const intraday = isIntradayDisciplineRow(row);
const expandHint = `<div class="card-expand-hint">${esc(HUB_EXPAND_HINT)}</div>`;
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 === "perp" || layout.splitSide) {
/* 桌面分栏永续/分所卡:预留仓位行+监控位行+全屏提示;不显示期权 N仓 */
if (layout.okxPart === "options") {
inner += renderOptionsSectionBody(row);
inner += expandHint;
return inner;
}
if (layout.okxPart === "perp" || !rowHasOptionsLayout(row)) {
inner += renderAccountStatRow(row, ag);
inner += renderPerpetualPositionsSection(row, ag, pos, orders, trends, tickMap, intraday);
inner += renderPerpetualKeyMonitorSlot(flaskOk ? keys : [], kmap || {});
inner += renderCardStrategyStats(row, hm, flaskOk, {
hideOptions: true,
hideKeyChips: true,
});
if (intraday) {
inner += `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`;
} else {
inner += expandHint;
}
return inner;
}
}
if (layout.okxPart === "options") {
inner += renderOptionsSectionBody(row);
inner += `<div class="card-expand-hint">点击标题栏进入全屏 · 期权专页可下单</div>`;
inner += expandHint;
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);
@@ -3895,7 +3935,7 @@
inner += renderCardStrategyStats(row, hm, flaskOk);
inner += intraday
? `<div class="card-expand-hint">日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}</div>`
: `<div class="card-expand-hint">点击标题栏进入全屏 · 委托 / 关键位 / 下单监控 / 趋势回调 / 顺势加仓</div>`;
: expandHint;
return inner;
}
+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-okx-2x2" />
<link rel="stylesheet" href="/assets/app.css?v=20260716-hub-2x2-slots" />
<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-okx-2x2"></script>
<script src="/assets/app.js?v=20260716-hub-2x2-slots"></script>
</body>
</html>