总览从在线客户端快照展示 ETH/BTC 报价,优先 live。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-04 19:52:56 +08:00
parent bcd32a25c3
commit 2c21f5e675
4 changed files with 177 additions and 4 deletions
+38 -2
View File
@@ -168,8 +168,39 @@ body.page-login .main {
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.overview-bj-clock { .overview-quotes {
margin-left: auto; margin-left: auto;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
font-variant-numeric: tabular-nums;
}
.overview-quote-row {
display: grid;
grid-template-columns: 2.4em minmax(4.6em, auto) minmax(3.4em, auto) minmax(3.6em, auto);
align-items: center;
column-gap: 8px;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
}
.overview-quote-sym { color: var(--muted); }
.overview-quote-last,
.overview-quote-chg,
.overview-quote-pct { text-align: right; }
.overview-quote-row.is-up .overview-quote-last,
.overview-quote-row.is-up .overview-quote-chg,
.overview-quote-row.is-up .overview-quote-pct { color: var(--ok); }
.overview-quote-row.is-down .overview-quote-last,
.overview-quote-row.is-down .overview-quote-chg,
.overview-quote-row.is-down .overview-quote-pct { color: var(--bad); }
.overview-quote-row.is-flat .overview-quote-last,
.overview-quote-row.is-flat .overview-quote-chg,
.overview-quote-row.is-flat .overview-quote-pct { color: var(--text); }
.overview-bj-clock {
margin-left: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -236,10 +267,15 @@ body.page-login .main {
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.page-head { flex-wrap: wrap; } .page-head { flex-wrap: wrap; }
.overview-bj-clock { .overview-quotes {
margin-left: 0; margin-left: 0;
order: 3; order: 3;
width: 100%; width: 100%;
}
.overview-bj-clock {
margin-left: 0;
order: 4;
width: 100%;
justify-content: flex-start; justify-content: flex-start;
padding-top: 2px; padding-top: 2px;
} }
+123
View File
@@ -566,6 +566,127 @@
}); });
} }
function fmtQuotePx(n) {
const v = Number(n);
if (!Number.isFinite(v)) return "—";
const abs = Math.abs(v);
let digits = 2;
if (abs >= 10000) digits = 1;
else if (abs >= 1000) digits = 2;
else if (abs >= 1) digits = 2;
else digits = 4;
try {
return v.toLocaleString("en-US", {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
} catch (e) {
return v.toFixed(digits);
}
}
function fmtQuoteSigned(n, isPct) {
const v = Number(n);
if (!Number.isFinite(v)) return "—";
const abs = Math.abs(v);
const digits = isPct ? 2 : abs >= 100 ? 1 : 2;
let body;
try {
body = abs.toLocaleString("en-US", {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
} catch (e) {
body = abs.toFixed(digits);
}
const sign = v > 0 ? "+" : v < 0 ? "-" : "";
return sign + body + (isPct ? "%" : "");
}
function quoteDirClass(n) {
const v = Number(n);
if (!Number.isFinite(v) || v === 0) return "is-flat";
return v > 0 ? "is-up" : "is-down";
}
function snapshotTickers(row) {
const snap = (row && row.snapshot) || {};
const quotes = snap.quotes && typeof snap.quotes === "object" ? snap.quotes : {};
const list = quotes.tickers;
return Array.isArray(list) ? list : [];
}
function tickersHaveLast(tickers) {
return (tickers || []).some(function (t) {
return t && t.last != null && t.last !== "" && Number.isFinite(Number(t.last));
});
}
function pickQuoteSource(list) {
let best = null;
(list || []).forEach(function (row) {
if (!row || !row.online) return;
const tickers = snapshotTickers(row);
if (!tickersHaveLast(tickers)) return;
const mode = String((((row.snapshot || {}).instance || {}).mode) || "").toLowerCase();
const live = mode === "live" ? 1 : 0;
const pulled = String(row.pulled_at || ((row.snapshot || {}).quotes || {}).updated_at || "");
const cand = { tickers: tickers, name: row.name || "", live: live, pulled: pulled };
if (!best) {
best = cand;
return;
}
if (cand.live !== best.live) {
if (cand.live > best.live) best = cand;
return;
}
if (cand.pulled > best.pulled) best = cand;
});
return best;
}
function paintOverviewQuotes(list) {
const root = document.getElementById("overview-quotes");
if (!root) return;
const src = pickQuoteSource(list);
const tickers = src ? src.tickers : [];
["ETH", "BTC"].forEach(function (base) {
const row = root.querySelector('.overview-quote-row[data-base="' + base + '"]');
if (!row) return;
let t = null;
for (let i = 0; i < tickers.length; i++) {
if (String((tickers[i] && tickers[i].base) || "").toUpperCase() === base) {
t = tickers[i];
break;
}
}
row.classList.remove("is-up", "is-down", "is-flat");
const lastEl = row.querySelector('[data-field="last"]');
const chgEl = row.querySelector('[data-field="change"]');
const pctEl = row.querySelector('[data-field="change_pct"]');
if (!t || t.last == null) {
row.classList.add("is-flat");
if (lastEl) lastEl.textContent = "—";
if (chgEl) chgEl.textContent = "—";
if (pctEl) pctEl.textContent = "—";
return;
}
row.classList.add(quoteDirClass(t.change));
if (lastEl) lastEl.textContent = fmtQuotePx(t.last);
if (chgEl) chgEl.textContent = fmtQuoteSigned(t.change, false);
if (pctEl) pctEl.textContent = fmtQuoteSigned(t.change_pct, true);
});
if (src && src.name) {
root.title =
"来自 " +
src.name +
(src.live ? " · live" : " · sim") +
" · 相对北京 08:00";
} else {
root.title = "暂无在线客户端报价 · 相对北京 08:00";
}
}
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");
@@ -577,6 +698,7 @@
if (!d.ok) throw new Error(d.msg || "失败"); if (!d.ok) throw new Error(d.msg || "失败");
const list = sortInstancesByMode(d.instances || []); const list = sortInstancesByMode(d.instances || []);
renderOverviewSummary(list); renderOverviewSummary(list);
paintOverviewQuotes(list);
if (!list.length) { if (!list.length) {
grid.innerHTML = grid.innerHTML =
'<p class="muted">暂无启用实例,请先到「实例」添加.</p>'; '<p class="muted">暂无启用实例,请先到「实例」添加.</p>';
@@ -611,6 +733,7 @@
box.hidden = true; box.hidden = true;
box.innerHTML = ""; box.innerHTML = "";
} }
paintOverviewQuotes([]);
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}期权中控{% endblock %}</title> <title>{% block title %}期权中控{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=21"> <link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=22">
</head> </head>
<body class="{% if request.path.startswith('/login') %}page-login{% elif request.path.startswith('/desk') %}page-desk{% elif request.path.startswith('/stats') %}page-stats{% elif request.path.startswith('/instances') %}page-instances{% elif request.path == '/' %}page-dashboard{% endif %}"> <body class="{% if request.path.startswith('/login') %}page-login{% elif request.path.startswith('/desk') %}page-desk{% elif request.path.startswith('/stats') %}page-stats{% elif request.path.startswith('/instances') %}page-instances{% elif request.path == '/' %}page-dashboard{% endif %}">
{% if session.get('logged_in') %} {% if session.get('logged_in') %}
+15 -1
View File
@@ -3,6 +3,20 @@
{% block body %} {% block body %}
<section class="page-head"> <section class="page-head">
<h1>期权持仓总览</h1> <h1>期权持仓总览</h1>
<div class="overview-quotes" id="overview-quotes" title="来自在线客户端 · 相对北京 08:00">
<div class="overview-quote-row" data-base="ETH">
<span class="overview-quote-sym">ETH</span>
<span class="overview-quote-last" data-field="last">—</span>
<span class="overview-quote-chg" data-field="change">—</span>
<span class="overview-quote-pct" data-field="change_pct">—</span>
</div>
<div class="overview-quote-row" data-base="BTC">
<span class="overview-quote-sym">BTC</span>
<span class="overview-quote-last" data-field="last">—</span>
<span class="overview-quote-chg" data-field="change">—</span>
<span class="overview-quote-pct" data-field="change_pct">—</span>
</div>
</div>
<div class="overview-bj-clock" id="overview-bj-clock" title="Asia/Shanghai"> <div class="overview-bj-clock" id="overview-bj-clock" title="Asia/Shanghai">
<span class="overview-bj-label">北京时间</span> <span class="overview-bj-label">北京时间</span>
<time class="overview-bj-time" id="overview-bj-time" datetime="">--</time> <time class="overview-bj-time" id="overview-bj-time" datetime="">--</time>
@@ -15,6 +29,6 @@
<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=20"></script> <script src="{{ url_for('static', filename='hub.js') }}?v=21"></script>
<script>HubOverview.boot();</script> <script>HubOverview.boot();</script>
{% endblock %} {% endblock %}