cf347e7b6b
Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
4.7 KiB
JavaScript
153 lines
4.7 KiB
JavaScript
(function (global) {
|
|
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 >= 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 "nav-spot--flat";
|
|
return v > 0 ? "nav-spot--up" : "nav-spot--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 paintRoot(root, tickers, src) {
|
|
["ETH", "BTC"].forEach(function (base) {
|
|
const row = root.querySelector('.nav-spot-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("nav-spot--up", "nav-spot--down", "nav-spot--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("nav-spot--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";
|
|
}
|
|
}
|
|
|
|
function paint(list) {
|
|
const src = pickQuoteSource(list);
|
|
const tickers = src ? src.tickers : [];
|
|
document.querySelectorAll("[data-hub-quotes]").forEach(function (root) {
|
|
paintRoot(root, tickers, src);
|
|
});
|
|
}
|
|
|
|
let timer = null;
|
|
let inflight = false;
|
|
|
|
async function refresh() {
|
|
if (inflight) return;
|
|
inflight = true;
|
|
try {
|
|
const r = await fetch("/api/overview");
|
|
const d = await r.json();
|
|
paint((d && d.instances) || []);
|
|
} catch (e) {
|
|
paint([]);
|
|
} finally {
|
|
inflight = false;
|
|
}
|
|
}
|
|
|
|
function boot() {
|
|
if (!document.querySelector("[data-hub-quotes]")) return;
|
|
if (global.HubOverview) return;
|
|
refresh();
|
|
if (timer) clearInterval(timer);
|
|
timer = setInterval(refresh, 10000);
|
|
}
|
|
|
|
global.HubQuotes = { boot: boot, paint: paint, refresh: refresh };
|
|
})(window);
|