总览增加实时汇总:在线/离线、仓位、浮盈、权利金、余额,并按 sim/live 分列。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-03 03:37:12 +08:00
parent 446fb1a1f3
commit 3f805534fb
3 changed files with 251 additions and 2 deletions
+68
View File
@@ -327,6 +327,74 @@ button, .btn {
.overview-table .status-off { color: var(--bad); }
.overview-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.overview-actions .btn { margin-top: 0; }
.overview-summary {
display: grid;
gap: 12px;
margin: 0 0 14px;
}
.overview-kpi-split {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.overview-kpi-block {
padding: 12px 14px;
border-radius: var(--radius);
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.overview-kpi-block--sim {
border-color: rgba(56, 189, 248, 0.28);
}
.overview-kpi-block--live {
border-color: rgba(52, 211, 153, 0.28);
}
.overview-kpi-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.overview-kpi-head strong {
font-size: 0.92rem;
letter-spacing: 0.02em;
}
.overview-kpi-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}
.overview-kpi-card {
padding: 8px 10px;
border-radius: 10px;
border: 1px solid rgba(56, 189, 248, 0.12);
background: rgba(5, 12, 22, 0.5);
min-width: 0;
}
.overview-kpi-label {
font-size: 0.72rem;
color: var(--muted);
margin-bottom: 4px;
}
.overview-kpi-value {
font-size: 0.95rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
word-break: break-word;
}
.overview-kpi-value.status-on { color: var(--ok); }
.overview-kpi-value.status-off { color: var(--bad); }
@media (max-width: 1100px) {
.overview-kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 800px) {
.overview-kpi-split { grid-template-columns: 1fr; }
.overview-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.pos-brief {
margin: 0;
padding-left: 0;
+181 -1
View File
@@ -289,6 +289,171 @@
}
}
function numOrNull(v) {
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
function emptyAgg() {
return {
machines: 0,
online: 0,
offline: 0,
positions: 0,
sheets: 0,
pnl: 0,
premium: 0,
cash: 0,
hasPnl: false,
hasPremium: false,
hasCash: false,
};
}
function addOnlineMetrics(bucket, snap) {
const brief = snap.positions_brief || snap.positions || [];
bucket.positions += brief.length;
brief.forEach(function (p) {
const sheets = numOrNull(p && p.sheets);
if (sheets != null) bucket.sheets += sheets;
});
const pnl =
snap.pnl && snap.pnl.unrealized_usdt != null
? numOrNull(snap.pnl.unrealized_usdt)
: numOrNull(snap.upl_total_usdc);
if (pnl != null) {
bucket.pnl += pnl;
bucket.hasPnl = true;
}
const prem = snap.premium || {};
const paid = numOrNull(prem.paid_usdt);
const cash = numOrNull(prem.cash_usdt);
if (paid != null) {
bucket.premium += paid;
bucket.hasPremium = true;
}
if (cash != null) {
bucket.cash += cash;
bucket.hasCash = true;
}
}
function summarizeOverview(list) {
const all = emptyAgg();
const sim = emptyAgg();
const live = emptyAgg();
(list || []).forEach(function (row) {
const snap = row.snapshot || {};
const mode = String((snap.instance || {}).mode || "").toLowerCase();
const online = !!row.online;
const targets = [all];
if (mode === "sim") targets.push(sim);
else if (mode === "live") targets.push(live);
targets.forEach(function (b) {
b.machines += 1;
if (online) b.online += 1;
else b.offline += 1;
});
if (!online) return;
addOnlineMetrics(all, snap);
if (mode === "sim") addOnlineMetrics(sim, snap);
else if (mode === "live") addOnlineMetrics(live, snap);
});
return { all: all, sim: sim, live: live };
}
function fmtMoneyPlain(v, known) {
if (!known || v == null || !Number.isFinite(Number(v))) return "—";
return Number(v).toFixed(2) + " U";
}
function renderAggCards(bucket) {
const posText =
bucket.positions > 0
? bucket.positions +
" 笔" +
(bucket.sheets > 0 ? " / " + Math.round(bucket.sheets) + " 张" : "")
: "0";
const items = [
{
label: "在线",
value: String(bucket.online),
cls: "status-on",
},
{
label: "离线",
value: String(bucket.offline),
cls: bucket.offline > 0 ? "status-off" : "",
},
{ label: "仓位合计", value: posText },
{
label: "浮盈合计",
value: bucket.hasPnl ? fmtPnl(bucket.pnl) : "—",
cls: bucket.hasPnl ? pnlClass(bucket.pnl) : "",
},
{
label: "权利金合计",
value: fmtMoneyPlain(bucket.premium, bucket.hasPremium),
},
{
label: "余额合计",
value: fmtMoneyPlain(bucket.cash, bucket.hasCash),
},
];
return items
.map(function (it) {
return (
'<div class="overview-kpi-card">' +
'<div class="overview-kpi-label">' +
escapeHtml(it.label) +
"</div>" +
'<div class="overview-kpi-value ' +
escapeHtml(it.cls || "") +
'">' +
escapeHtml(it.value) +
"</div></div>"
);
})
.join("");
}
function renderOverviewSummary(list) {
const box = document.getElementById("overview-summary");
if (!box) return;
if (!list || !list.length) {
box.hidden = true;
box.innerHTML = "";
return;
}
const s = summarizeOverview(list);
box.hidden = false;
box.innerHTML =
'<div class="overview-kpi-block">' +
'<div class="overview-kpi-head"><strong>全部</strong><span class="muted">' +
s.all.machines +
" 台</span></div>" +
'<div class="overview-kpi-grid">' +
renderAggCards(s.all) +
"</div></div>" +
'<div class="overview-kpi-split">' +
'<div class="overview-kpi-block overview-kpi-block--sim">' +
'<div class="overview-kpi-head"><strong>模拟 sim</strong><span class="muted">' +
s.sim.machines +
" 台</span></div>" +
'<div class="overview-kpi-grid">' +
renderAggCards(s.sim) +
"</div></div>" +
'<div class="overview-kpi-block overview-kpi-block--live">' +
'<div class="overview-kpi-head"><strong>实盘 live</strong><span class="muted">' +
s.live.machines +
" 台</span></div>" +
'<div class="overview-kpi-grid">' +
renderAggCards(s.live) +
"</div></div>" +
"</div>";
}
async function refresh() {
const status = document.getElementById("overview-status");
const grid = document.getElementById("overview-grid");
@@ -299,6 +464,7 @@
const d = await r.json();
if (!d.ok) throw new Error(d.msg || "失败");
const list = d.instances || [];
renderOverviewSummary(list);
if (!list.length) {
grid.innerHTML =
'<p class="muted">暂无启用实例,请先到「实例」添加.</p>';
@@ -325,9 +491,23 @@
});
});
}
if (status) status.textContent = "已更新 · 共 " + list.length + " 台";
if (status) {
const s = summarizeOverview(list);
status.textContent =
"已更新 · 共 " +
list.length +
" 台 · 在线 " +
s.all.online +
" · 离线 " +
s.all.offline;
}
} catch (e) {
if (status) status.textContent = e.message || "加载失败";
const box = document.getElementById("overview-summary");
if (box) {
box.hidden = true;
box.innerHTML = "";
}
}
}
+2 -1
View File
@@ -10,9 +10,10 @@
<button type="button" class="btn" id="btn-refresh">刷新</button>
</section>
<p class="muted" id="overview-status">加载中…</p>
<section class="overview-summary" id="overview-summary" hidden></section>
<div id="overview-grid"></div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='hub.js') }}?v=3"></script>
<script src="{{ url_for('static', filename='hub.js') }}?v=4"></script>
<script>HubOverview.boot();</script>
{% endblock %}