const REFRESH_MS = 60_000; const tableState = { yesterday: { items: [], meta: {}, sortKey: "rank", sortDir: "asc", }, today: { items: [], meta: {}, sortKey: "rank", sortDir: "asc", }, }; const SORT_KEYS = { rank: (r) => Number(r.rank) || 0, symbol: (r) => String(r.symbol || ""), quote_volume: (r) => Number(r.quote_volume) || 0, price_change_pct: (r) => Number(r.price_change_pct) || 0, tags: (r) => { let score = 0; if (r.is_high_volume) score += 2; if (r.is_high_change) score += 1; return score; }, }; function formatPeriod(start, end) { const fmt = (s) => s.replace("T", " ").slice(0, 16); return `${fmt(start)} ~ ${fmt(end)}`; } function tagText(row) { const tags = []; if (row.is_high_volume) tags.push("千万+"); if (row.is_high_change) tags.push("涨跌5%+"); return tags.join(" ") || ""; } function renderTags(row) { const parts = []; if (row.is_high_volume) { parts.push('千万+'); } if (row.is_high_change) { parts.push('涨跌5%+'); } return parts.length ? parts.join("") : "—"; } function pctClass(pct) { if (pct > 0) return "pct-up"; if (pct < 0) return "pct-down"; return ""; } function sortItems(items, key, dir) { const getter = SORT_KEYS[key] || SORT_KEYS.rank; const sorted = [...items].sort((a, b) => { const va = getter(a); const vb = getter(b); if (typeof va === "string") { return dir === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); } return dir === "asc" ? va - vb : vb - va; }); return sorted; } function updateSortHeaders(tableId) { const table = document.querySelector(`table[data-table="${tableId}"]`); if (!table) return; const { sortKey, sortDir } = tableState[tableId]; table.querySelectorAll("th.sortable").forEach((th) => { th.classList.remove("sorted-asc", "sorted-desc"); const key = th.dataset.sort; if (key === sortKey) { th.classList.add(sortDir === "asc" ? "sorted-asc" : "sorted-desc"); } }); } function renderTable(tableId, tbody) { const state = tableState[tableId]; const items = sortItems(state.items, state.sortKey, state.sortDir); if (!items.length) { tbody.innerHTML = '暂无数据'; updateSortHeaders(tableId); return; } tbody.innerHTML = items .map((row, idx) => { const highlight = row.is_high_volume || row.is_high_change ? " row-highlight" : ""; const pct = row.price_change_pct ?? 0; const displayRank = state.sortKey === "rank" && state.sortDir === "asc" ? row.rank : idx + 1; return ` ${displayRank} ${row.symbol} ${row.quote_volume_fmt || row.quote_volume} ${row.price_change_pct_fmt || pct.toFixed(2) + "%"} ${renderTags(row)} `; }) .join(""); updateSortHeaders(tableId); } function setTableData(tableId, data) { tableState[tableId].items = data.items || []; tableState[tableId].meta = { period_start: data.period_start, period_end: data.period_end, updated_at: data.updated_at, }; const tbody = document.getElementById(`${tableId}-body`); renderTable(tableId, tbody); } function toggleSort(tableId, key) { const state = tableState[tableId]; if (state.sortKey === key) { state.sortDir = state.sortDir === "asc" ? "desc" : "asc"; } else { state.sortKey = key; state.sortDir = key === "symbol" ? "asc" : "desc"; } const tbody = document.getElementById(`${tableId}-body`); renderTable(tableId, tbody); } function resetSort(tableId) { tableState[tableId].sortKey = "rank"; tableState[tableId].sortDir = "asc"; const tbody = document.getElementById(`${tableId}-body`); renderTable(tableId, tbody); } function exportCsv(tableId) { const state = tableState[tableId]; if (!state.items.length) { alert("暂无数据可导出"); return; } const items = sortItems(state.items, state.sortKey, state.sortDir); const header = [ "排名", "合约", "成交额显示", "成交额USDT", "涨跌幅%", "千万+", "涨跌5%+", "标记", ]; const rows = items.map((r, i) => [ state.sortKey === "rank" && state.sortDir === "asc" ? r.rank : i + 1, r.symbol, r.quote_volume_fmt || "", r.quote_volume ?? "", r.price_change_pct ?? "", r.is_high_volume ? "是" : "否", r.is_high_change ? "是" : "否", tagText(r), ]); const escape = (v) => `"${String(v).replace(/"/g, '""')}"`; const csv = [header, ...rows].map((row) => row.map(escape).join(",")).join("\n"); const blob = new Blob(["\ufeff" + csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const period = state.meta.period_start ? state.meta.period_start.slice(0, 10) : tableId; a.href = url; a.download = `binance-top30-${tableId}-${period}.csv`; a.click(); URL.revokeObjectURL(url); } document.querySelectorAll("th.sortable").forEach((th) => { th.addEventListener("click", () => { const table = th.closest("table"); const tableId = table?.dataset.table; const key = th.dataset.sort; if (tableId && key) toggleSort(tableId, key); }); }); document.querySelectorAll("[data-export]").forEach((btn) => { btn.addEventListener("click", () => exportCsv(btn.dataset.export)); }); document.querySelectorAll("[data-reset]").forEach((btn) => { btn.addEventListener("click", () => resetSort(btn.dataset.reset)); }); async function loadYesterday() { const body = document.getElementById("yesterday-body"); body.innerHTML = '加载中…'; try { const res = await fetch("/api/yesterday/top30"); const data = await res.json(); document.getElementById("yesterday-period").textContent = formatPeriod( data.period_start, data.period_end ); document.getElementById("yesterday-updated").textContent = "更新: " + (data.updated_at || "").replace("T", " ").slice(0, 19); setTableData("yesterday", data); } catch (e) { body.innerHTML = `加载失败: ${e.message}`; } } async function loadToday() { const body = document.getElementById("today-body"); try { const res = await fetch("/api/today/top30"); const data = await res.json(); document.getElementById("today-period").textContent = formatPeriod( data.period_start, data.period_end ); document.getElementById("today-updated").textContent = "更新: " + (data.updated_at || "").replace("T", " ").slice(0, 19); setTableData("today", data); document.getElementById("status").textContent = "今日数据已刷新"; } catch (e) { body.innerHTML = `加载失败: ${e.message}`; document.getElementById("status").textContent = e.message; } } document.getElementById("btn-refresh").addEventListener("click", async () => { document.getElementById("status").textContent = "刷新中…"; await fetch("/api/refresh/today", { method: "POST" }); await loadToday(); }); loadYesterday(); loadToday(); setInterval(loadToday, REFRESH_MS);