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 对齐)
### 修改原因
File diff suppressed because it is too large Load Diff
+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>