Add BTC/ETH spot tickers on nav, anchored to Beijing 08:00 open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-17 11:42:19 +08:00
parent 960565482f
commit 08029ccb15
9 changed files with 472 additions and 14 deletions
+3 -1
View File
@@ -10,7 +10,9 @@
.header h1{font-size:1.75rem;color:#dbe4ff;text-align:center;line-height:1.25}
.exchange-tag{font-size:.82rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:5px 14px;border-radius:999px;letter-spacing:.06em}
.header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
.top-nav{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:12px}
.top-nav{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:10px 12px;margin-bottom:12px}
.top-nav-links{grid-column:2;display:flex;gap:8px;flex-wrap:wrap;justify-content:center;min-width:0}
.top-nav-spot-tickers{grid-column:3;justify-self:end;display:flex;flex-direction:column;gap:3px;min-width:0}
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
.stat-box{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-bottom:16px;align-items:stretch}
+153 -6
View File
@@ -54,21 +54,43 @@ html[data-theme="light"] {
.top-nav {
display: flex !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
justify-content: flex-start !important;
align-items: stretch;
overflow-x: auto !important;
overflow-y: hidden;
align-items: stretch !important;
overflow: visible !important;
width: 100%;
max-width: 100%;
gap: 8px !important;
margin-bottom: 12px !important;
padding: 2px 2px 6px;
}
.top-nav-links {
display: flex;
flex-wrap: nowrap;
overflow-x: auto !important;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
gap: 6px !important;
margin-bottom: 12px !important;
padding: 2px 2px 6px;
gap: 6px;
scroll-padding-inline: 10px;
touch-action: pan-x;
min-width: 0;
}
.top-nav-links::-webkit-scrollbar {
display: none;
}
.top-nav-spot-tickers {
justify-self: stretch;
align-self: stretch;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.top-nav::-webkit-scrollbar {
@@ -633,6 +655,131 @@ html[data-theme="light"] .theme-toggle-btn.is-active {
font-size: 0.82rem;
}
/* 导航右侧 BTC/ETH 现货(相对北京 08:00 开盘) */
.top-nav-links {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
min-width: 0;
}
.top-nav-spot-tickers {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
font-variant-numeric: tabular-nums;
}
.nav-spot-row {
display: grid;
grid-template-columns: 3px 16px minmax(4.2em, auto) minmax(4.5em, auto) minmax(3.2em, auto) minmax(3.4em, auto);
align-items: center;
column-gap: 6px;
padding: 2px 8px 2px 4px;
border: 1px solid transparent;
border-radius: 6px;
background: rgba(16, 22, 36, 0.72);
line-height: 1.15;
white-space: nowrap;
font-size: 0.72rem;
}
.nav-spot-row:hover {
border-color: #3a4568;
}
.nav-spot-ribbon {
width: 3px;
height: 14px;
border-radius: 1px 2px 2px 1px;
background: #e24b6a;
justify-self: center;
}
.nav-spot-icon {
width: 16px;
height: 16px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.62rem;
font-weight: 700;
color: #fff;
}
.nav-spot-icon--eth {
background: #627eea;
}
.nav-spot-icon--btc {
background: #f2a900;
color: #1a1200;
}
.nav-spot-sym {
color: var(--inst-text, #e8ecff);
font-weight: 600;
letter-spacing: 0.01em;
}
.nav-spot-last,
.nav-spot-chg,
.nav-spot-pct {
text-align: right;
font-weight: 600;
}
.nav-spot--up .nav-spot-last,
.nav-spot--up .nav-spot-chg,
.nav-spot--up .nav-spot-pct {
color: #2dd4a8;
}
.nav-spot--down .nav-spot-last,
.nav-spot--down .nav-spot-chg,
.nav-spot--down .nav-spot-pct {
color: #ff6b7a;
}
.nav-spot--flat .nav-spot-last,
.nav-spot--flat .nav-spot-chg,
.nav-spot--flat .nav-spot-pct {
color: #9aa3bf;
}
html[data-theme="light"] .nav-spot-row {
background: rgba(255, 255, 255, 0.85);
border-color: #d5e0ec;
}
html[data-theme="light"] .nav-spot-row:hover {
border-color: #9bb4cc;
}
html[data-theme="light"] .nav-spot--up .nav-spot-last,
html[data-theme="light"] .nav-spot--up .nav-spot-chg,
html[data-theme="light"] .nav-spot--up .nav-spot-pct {
color: #0a8f6c;
}
html[data-theme="light"] .nav-spot--down .nav-spot-last,
html[data-theme="light"] .nav-spot--down .nav-spot-chg,
html[data-theme="light"] .nav-spot--down .nav-spot-pct {
color: #d12b3a;
}
@media (max-width: 1100px) {
.nav-spot-row {
grid-template-columns: 3px 14px minmax(3.6em, auto) minmax(3.8em, auto) minmax(2.8em, auto) minmax(3em, auto);
column-gap: 4px;
font-size: 0.66rem;
padding: 2px 6px 2px 3px;
}
}
.instance-header-stats-wrap {
margin-top: 12px;
padding-top: 12px;
+2 -1
View File
@@ -262,7 +262,8 @@
const mq = window.matchMedia("(max-width: 720px)");
function scrollActiveTab(nav) {
const active = nav.querySelector("a.active");
const scroller = nav.querySelector(".top-nav-links") || nav;
const active = scroller.querySelector("a.active");
if (!active) return;
requestAnimationFrame(() => {
try {
+120
View File
@@ -0,0 +1,120 @@
/**
* 导航栏 BTC/ETH 现货报价:相对北京 08:00 开盘涨跌.
*/
(function (global) {
"use strict";
const DEFAULT_POLL_MS = 5000;
let timer = null;
let inflight = false;
function pollMs() {
const raw =
(document.body && document.body.getAttribute("data-price-refresh-ms")) || "";
const n = Number(raw);
return Number.isFinite(n) && n >= 2000 ? n : DEFAULT_POLL_MS;
}
function fmtPrice(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 >= 100) digits = 2;
else if (abs >= 1) digits = 3;
else digits = 4;
try {
return v.toLocaleString("en-US", {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
} catch (_) {
return v.toFixed(digits);
}
}
function fmtSigned(n, isPct) {
const v = Number(n);
if (!Number.isFinite(v)) return "—";
const abs = Math.abs(v);
let digits = isPct ? 2 : abs >= 100 ? 1 : 2;
if (!isPct && abs < 1) digits = 3;
const body = abs.toLocaleString("en-US", {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
const sign = v > 0 ? "" : v < 0 ? "-" : "";
return sign + body + (isPct ? "%" : "");
}
function dirClass(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 paintRow(row, t) {
if (!row || !t) return;
const lastEl = row.querySelector('[data-field="last"]');
const chgEl = row.querySelector('[data-field="change"]');
const pctEl = row.querySelector('[data-field="change_pct"]');
const dir = dirClass(t.change);
row.classList.remove("nav-spot--up", "nav-spot--down", "nav-spot--flat");
row.classList.add(dir);
if (lastEl) lastEl.textContent = fmtPrice(t.last);
if (chgEl) chgEl.textContent = fmtSigned(t.change, false);
if (pctEl) pctEl.textContent = fmtSigned(t.change_pct, true);
const openHint =
t.open != null
? "开盘 " + fmtPrice(t.open) + " (" + (t.anchor || "BJ 08:00") + ")"
: t.anchor || "BJ 08:00";
row.title = (t.symbol || "") + " " + openHint;
}
function apply(data) {
const root = document.getElementById("nav-spot-tickers");
if (!root) return;
const list = (data && data.tickers) || [];
list.forEach(function (t) {
const base = (t.base || "").toUpperCase();
if (!base) return;
const row = root.querySelector('.nav-spot-row[data-base="' + base + '"]');
paintRow(row, t);
});
}
function refresh() {
const root = document.getElementById("nav-spot-tickers");
if (!root || inflight) return;
inflight = true;
fetch("/api/nav_spot_tickers")
.then(function (r) {
return r.json();
})
.then(function (data) {
if (data && data.ok !== false) apply(data);
})
.catch(function () {})
.finally(function () {
inflight = false;
});
}
function start() {
if (!document.getElementById("nav-spot-tickers")) return;
refresh();
if (timer) clearInterval(timer);
timer = setInterval(refresh, pollMs());
}
global.refreshNavSpotTickers = refresh;
global.startNavSpotTickers = start;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})(window);