总览增加实时汇总:在线/离线、仓位、浮盈、权利金、余额,并按 sim/live 分列。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -327,6 +327,74 @@ button, .btn {
|
|||||||
.overview-table .status-off { color: var(--bad); }
|
.overview-table .status-off { color: var(--bad); }
|
||||||
.overview-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
.overview-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
||||||
.overview-actions .btn { margin-top: 0; }
|
.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 {
|
.pos-brief {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|||||||
+181
-1
@@ -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() {
|
async function refresh() {
|
||||||
const status = document.getElementById("overview-status");
|
const status = document.getElementById("overview-status");
|
||||||
const grid = document.getElementById("overview-grid");
|
const grid = document.getElementById("overview-grid");
|
||||||
@@ -299,6 +464,7 @@
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.ok) throw new Error(d.msg || "失败");
|
if (!d.ok) throw new Error(d.msg || "失败");
|
||||||
const list = d.instances || [];
|
const list = d.instances || [];
|
||||||
|
renderOverviewSummary(list);
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
grid.innerHTML =
|
grid.innerHTML =
|
||||||
'<p class="muted">暂无启用实例,请先到「实例」添加.</p>';
|
'<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) {
|
} catch (e) {
|
||||||
if (status) status.textContent = e.message || "加载失败";
|
if (status) status.textContent = e.message || "加载失败";
|
||||||
|
const box = document.getElementById("overview-summary");
|
||||||
|
if (box) {
|
||||||
|
box.hidden = true;
|
||||||
|
box.innerHTML = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
<button type="button" class="btn" id="btn-refresh">刷新</button>
|
<button type="button" class="btn" id="btn-refresh">刷新</button>
|
||||||
</section>
|
</section>
|
||||||
<p class="muted" id="overview-status">加载中…</p>
|
<p class="muted" id="overview-status">加载中…</p>
|
||||||
|
<section class="overview-summary" id="overview-summary" hidden></section>
|
||||||
<div id="overview-grid"></div>
|
<div id="overview-grid"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% 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>
|
<script>HubOverview.boot();</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user