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:
@@ -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 对齐)
|
||||
|
||||
### 修改原因
|
||||
|
||||
+10155
-10533
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user