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
+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@2x≈1280×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;
}