From 3f805534fb4386b2665596daaac650452bd8848e Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 3 Oct 2026 03:37:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=80=BB=E8=A7=88=E5=A2=9E=E5=8A=A0=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=B1=87=E6=80=BB=EF=BC=9A=E5=9C=A8=E7=BA=BF/?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E3=80=81=E4=BB=93=E4=BD=8D=E3=80=81=E6=B5=AE?= =?UTF-8?q?=E7=9B=88=E3=80=81=E6=9D=83=E5=88=A9=E9=87=91=E3=80=81=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=EF=BC=8C=E5=B9=B6=E6=8C=89=20sim/live=20=E5=88=86?= =?UTF-8?q?=E5=88=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- static/hub.css | 68 +++++++++++++++ static/hub.js | 182 ++++++++++++++++++++++++++++++++++++++- templates/dashboard.html | 3 +- 3 files changed, 251 insertions(+), 2 deletions(-) diff --git a/static/hub.css b/static/hub.css index 9c148fc..dac44d5 100644 --- a/static/hub.css +++ b/static/hub.css @@ -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; diff --git a/static/hub.js b/static/hub.js index 476e028..8eb1bd4 100644 --- a/static/hub.js +++ b/static/hub.js @@ -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 ( + '
' + + '
' + + escapeHtml(it.label) + + "
" + + '
' + + escapeHtml(it.value) + + "
" + ); + }) + .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 = + '
' + + '
全部' + + s.all.machines + + " 台
" + + '
' + + renderAggCards(s.all) + + "
" + + '
' + + '
' + + '
模拟 sim' + + s.sim.machines + + " 台
" + + '
' + + renderAggCards(s.sim) + + "
" + + '
' + + '
实盘 live' + + s.live.machines + + " 台
" + + '
' + + renderAggCards(s.live) + + "
" + + "
"; + } + 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 = '

暂无启用实例,请先到「实例」添加.

'; @@ -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 = ""; + } } } diff --git a/templates/dashboard.html b/templates/dashboard.html index 5debee3..93951bd 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -10,9 +10,10 @@

加载中…

+
{% endblock %} {% block scripts %} - + {% endblock %}