feat: 监控区 2x2 布局与左上今日统计卡
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -111,6 +111,7 @@
|
||||
}
|
||||
let tpslPending = null;
|
||||
let lastMonitorRows = [];
|
||||
let lastMonitorTotals = null;
|
||||
let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
|
||||
const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1";
|
||||
const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
||||
@@ -1307,15 +1308,16 @@
|
||||
}, 12000);
|
||||
}
|
||||
|
||||
function saveMonitorBoardCache(rows, updatedAt, boardVersion) {
|
||||
function saveMonitorBoardCache(rows, updatedAt, boardVersion, totals) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
HUB_MONITOR_BOARD_CACHE_KEY,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
version: 2,
|
||||
board_version: boardVersion != null ? boardVersion : localBoardVersion,
|
||||
updated_at: updatedAt || "",
|
||||
rows: rows || [],
|
||||
totals: totals || null,
|
||||
saved_at: Date.now(),
|
||||
})
|
||||
);
|
||||
@@ -1343,6 +1345,7 @@
|
||||
const cached = loadMonitorBoardFromCache();
|
||||
if (!cached) return false;
|
||||
lastMonitorRows = cached.rows;
|
||||
lastMonitorTotals = cached.totals || null;
|
||||
lastMonitorBoardUpdatedAt = cached.updated_at || "";
|
||||
localBoardVersion = 0;
|
||||
applyMonitorBoardUi(cached.rows, lastMonitorBoardUpdatedAt, { stale: true });
|
||||
@@ -1602,18 +1605,25 @@
|
||||
el.innerHTML = `<span class="mas-item mas-ok">正常 ${ok}</span><span class="mas-sep">·</span><span class="mas-item mas-warn">关注 ${warn}</span><span class="mas-sep">·</span><span class="mas-item mas-err">异常 ${err}</span>`;
|
||||
}
|
||||
|
||||
/** 监控卡片列数:桌面 3/2 列;手机端 2 列瓦片 */
|
||||
function syncMonitorGridColumns(gridEl, count) {
|
||||
/** 监控卡片列数:桌面 2×2(统计+三所);手机 2 列瓦片 */
|
||||
function syncMonitorGridColumns(gridEl, itemCount, opts) {
|
||||
if (!gridEl) return;
|
||||
const options = opts || {};
|
||||
if (isMobileLayout()) {
|
||||
gridEl.style.gridTemplateColumns = options.statsFirst
|
||||
? "1fr"
|
||||
: "repeat(2, minmax(0, 1fr))";
|
||||
return;
|
||||
}
|
||||
if (options.statsFirst) {
|
||||
gridEl.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))";
|
||||
return;
|
||||
}
|
||||
let cols = 3;
|
||||
if (count <= 1) cols = 1;
|
||||
else if (count === 2) cols = 2;
|
||||
else if (count === 3) cols = 3;
|
||||
else if (count === 4) cols = 2;
|
||||
if (itemCount <= 1) cols = 1;
|
||||
else if (itemCount === 2) cols = 2;
|
||||
else if (itemCount === 3) cols = 3;
|
||||
else if (itemCount === 4) cols = 2;
|
||||
else cols = 3;
|
||||
gridEl.style.gridTemplateColumns = `repeat(${cols}, minmax(0, 1fr))`;
|
||||
}
|
||||
@@ -1800,7 +1810,9 @@
|
||||
wasMobile = nowMobile;
|
||||
const box = document.getElementById("monitor-grid");
|
||||
if (box && lastMonitorRows.length) {
|
||||
syncMonitorGridColumns(box, lastMonitorRows.length);
|
||||
syncMonitorGridColumns(box, lastMonitorRows.length + (lastMonitorTotals ? 1 : 0), {
|
||||
statsFirst: !!lastMonitorTotals,
|
||||
});
|
||||
updateMonitorAlertSummary(lastMonitorRows);
|
||||
}
|
||||
}, 120);
|
||||
@@ -2031,7 +2043,8 @@
|
||||
if (versionChanged || timeChanged || !lastMonitorRows.length) {
|
||||
localBoardVersion = ver;
|
||||
lastMonitorRows = rows;
|
||||
saveMonitorBoardCache(lastMonitorRows, ts, ver);
|
||||
lastMonitorTotals = data.totals || null;
|
||||
saveMonitorBoardCache(lastMonitorRows, ts, ver, lastMonitorTotals);
|
||||
applyMonitorBoardUi(lastMonitorRows, ts, {
|
||||
stale: !!data.aggregating,
|
||||
});
|
||||
@@ -2092,6 +2105,64 @@
|
||||
renderMonitorGrid(lastMonitorRows);
|
||||
}
|
||||
|
||||
function pnlSigned(v, decimals) {
|
||||
const n = Number(v);
|
||||
const d = decimals == null ? 2 : decimals;
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
if (Math.abs(n) < 1e-12) return fmt(0, d);
|
||||
const abs = fmt(Math.abs(n), d);
|
||||
return (n > 0 ? "+" : "-") + abs;
|
||||
}
|
||||
|
||||
function renderMonitorStatsCard(totals) {
|
||||
const t = totals || {};
|
||||
const day = t.trading_day || "—";
|
||||
const resetH = t.reset_hour != null ? t.reset_hour : 8;
|
||||
const winN = Number(t.win_count) || 0;
|
||||
const lossN = Number(t.loss_count) || 0;
|
||||
function cell(label, main, sub, valCls) {
|
||||
return `<div class="monitor-stat-cell">
|
||||
<div class="monitor-stat-label">${esc(label)}</div>
|
||||
<div class="monitor-stat-value ${valCls || ""}">${main}</div>
|
||||
${sub ? `<div class="monitor-stat-sub">${sub}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
const winSub =
|
||||
winN > 0 && Number.isFinite(Number(t.win_pnl_u))
|
||||
? `<span class="${pnlCls(t.win_pnl_u)}">${esc(pnlSigned(t.win_pnl_u, 2))}U</span>`
|
||||
: "—";
|
||||
const lossSub =
|
||||
lossN > 0 && Number.isFinite(Number(t.loss_pnl_u))
|
||||
? `<span class="${pnlCls(t.loss_pnl_u)}">${esc(pnlSigned(t.loss_pnl_u, 2))}U</span>`
|
||||
: "—";
|
||||
const floatVal = Number(t.float_pnl_u);
|
||||
return `<div class="card card-online monitor-stats-card" data-monitor-stats="1">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<div class="card-title-row">
|
||||
<div class="card-title">今日统计</div>
|
||||
</div>
|
||||
<div class="card-sub">交易日 ${esc(day)} · 北京时间 ${esc(String(resetH))}:00 切日</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="monitor-stats-grid">
|
||||
${cell("今日开仓", String(Number(t.open_count) || 0), "含未平", "")}
|
||||
${cell("今日平仓", String(Number(t.closed_count) || 0), "", "")}
|
||||
${cell("持有仓位", String(Number(t.open_position_count) || 0), "", "")}
|
||||
${cell("盈利", String(winN), winSub, "pnl-pos")}
|
||||
${cell("亏损", String(lossN), lossSub, lossN > 0 ? "pnl-neg" : "")}
|
||||
${cell(
|
||||
"总浮盈亏",
|
||||
esc(pnlSigned(floatVal, 2)) + "U",
|
||||
"",
|
||||
pnlCls(floatVal)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderMonitorGrid(rows) {
|
||||
const box = document.getElementById("monitor-grid");
|
||||
const fs = document.getElementById("exchange-fullscreen");
|
||||
@@ -2102,17 +2173,27 @@
|
||||
}
|
||||
const mobileTiles = isMobileLayout() && !expandedExchangeId;
|
||||
const displayRows = mobileTiles ? sortRowsForMobileDashboard(rows) : rows;
|
||||
const showStatsCard = !expandedExchangeId;
|
||||
box.classList.toggle("grid-monitor-tiles", mobileTiles);
|
||||
box.classList.toggle("grid-monitor-2x2", showStatsCard && !mobileTiles);
|
||||
box.classList.toggle("grid-monitor-with-stats", showStatsCard && mobileTiles);
|
||||
try {
|
||||
box.innerHTML =
|
||||
const statsHtml = showStatsCard ? renderMonitorStatsCard(lastMonitorTotals) : "";
|
||||
const cardsHtml =
|
||||
displayRows
|
||||
.map((r) => (mobileTiles ? renderMonitorTile(r) : renderMonitorCard(r)))
|
||||
.join("") || '<div class="err">无已启用账户</div>';
|
||||
.join("") || (showStatsCard ? "" : '<div class="err">无已启用账户</div>');
|
||||
box.innerHTML = statsHtml + cardsHtml;
|
||||
if (showStatsCard && !cardsHtml && !statsHtml) {
|
||||
box.innerHTML = '<div class="err">无已启用账户</div>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("renderMonitorGrid", err);
|
||||
box.innerHTML = `<div class="err">监控区渲染失败:${esc(String(err && err.message ? err.message : err))}</div>`;
|
||||
}
|
||||
syncMonitorGridColumns(box, displayRows.length);
|
||||
syncMonitorGridColumns(box, displayRows.length + (showStatsCard ? 1 : 0), {
|
||||
statsFirst: showStatsCard,
|
||||
});
|
||||
bindMonitorInteractions(box);
|
||||
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
|
||||
TimeCloseUI.tickLocalCountdowns();
|
||||
|
||||
Reference in New Issue
Block a user