From 08b840c0c5956c18350f53608bd388ea6bd85ff2 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 22 May 2026 13:27:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/app.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++--- web/index.html | 34 ++++++---- web/style.css | 52 +++++++++++++- 3 files changed, 242 insertions(+), 23 deletions(-) diff --git a/web/app.js b/web/app.js index c010308..591a8ac 100644 --- a/web/app.js +++ b/web/app.js @@ -1,10 +1,45 @@ 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) { @@ -22,27 +57,153 @@ function pctClass(pct) { return ""; } -function renderTable(tbody, items) { - if (!items || !items.length) { +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) => { + .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 ` - ${row.rank} + ${displayRank} ${row.symbol} - ${row.quote_volume_fmt || row.quote_volume} - ${row.price_change_pct_fmt || pct.toFixed(2) + "%"} - ${renderTags(row)} + ${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 = '加载中…'; @@ -55,7 +216,7 @@ async function loadYesterday() { ); document.getElementById("yesterday-updated").textContent = "更新: " + (data.updated_at || "").replace("T", " ").slice(0, 19); - renderTable(body, data.items); + setTableData("yesterday", data); } catch (e) { body.innerHTML = `加载失败: ${e.message}`; } @@ -72,7 +233,7 @@ async function loadToday() { ); document.getElementById("today-updated").textContent = "更新: " + (data.updated_at || "").replace("T", " ").slice(0, 19); - renderTable(body, data.items); + setTableData("today", data); document.getElementById("status").textContent = "今日数据已刷新"; } catch (e) { body.innerHTML = `加载失败: ${e.message}`; diff --git a/web/index.html b/web/index.html index 4c96193..1fdf472 100644 --- a/web/index.html +++ b/web/index.html @@ -9,7 +9,7 @@

币安 U本位合约 · 成交额排名

-

北京时间 08:00 切日 · Top30 · 高亮:≥1000万 USDT / |涨跌|≥5%

+

北京时间 08:00 切日 · Top30 · 高亮:≥1000万 USDT / |涨跌|≥5% · 点击表头可排序

@@ -17,16 +17,20 @@

昨日周期

+
+ + +
- +
- - - - - + + + + + @@ -39,16 +43,20 @@

今日周期 实时

+
+ + +
-
排名合约成交额 (USDT)涨跌幅标记排名合约成交额 (USDT)涨跌幅标记
+
- - - - - + + + + + diff --git a/web/style.css b/web/style.css index d0f0740..2273a3c 100644 --- a/web/style.css +++ b/web/style.css @@ -71,7 +71,6 @@ header h1 { } .updated { - margin-left: auto; color: var(--muted); font-size: 0.8rem; } @@ -99,6 +98,57 @@ th { font-size: 0.8rem; } +th.sortable { + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +th.sortable:hover { + color: var(--text); +} + +th.sortable::after { + content: " ⇅"; + font-size: 0.7rem; + opacity: 0.35; +} + +th.sorted-asc::after { + content: " ↑"; + opacity: 1; + color: var(--accent); +} + +th.sorted-desc::after { + content: " ↓"; + opacity: 1; + color: var(--accent); +} + +.panel-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-left: auto; +} + +.btn-secondary { + background: transparent; + color: var(--text); + border: 1px solid var(--border); + padding: 0.35rem 0.75rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + font-weight: 500; +} + +.btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + tr:hover td { background: rgba(255, 255, 255, 0.03); }
排名合约成交额 (USDT)涨跌幅标记排名合约成交额 (USDT)涨跌幅标记