(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);