增加排序
This commit is contained in:
+170
-9
@@ -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 = '<tr><td colspan="5" class="loading">暂无数据</td></tr>';
|
||||
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 `<tr class="${highlight}">
|
||||
<td class="rank">${row.rank}</td>
|
||||
<td class="rank">${displayRank}</td>
|
||||
<td><strong>${row.symbol}</strong></td>
|
||||
<td>${row.quote_volume_fmt || row.quote_volume}</td>
|
||||
<td class="${pctClass(pct)}">${row.price_change_pct_fmt || pct.toFixed(2) + "%"}</td>
|
||||
<td>${renderTags(row)}</td>
|
||||
<td data-value="${row.quote_volume ?? 0}">${row.quote_volume_fmt || row.quote_volume}</td>
|
||||
<td class="${pctClass(pct)}" data-value="${pct}">${row.price_change_pct_fmt || pct.toFixed(2) + "%"}</td>
|
||||
<td data-value="${tagText(row)}">${renderTags(row)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.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 = '<tr><td colspan="5" class="loading">加载中…</td></tr>';
|
||||
@@ -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 = `<tr><td colspan="5" class="error">加载失败: ${e.message}</td></tr>`;
|
||||
}
|
||||
@@ -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 = `<tr><td colspan="5" class="error">加载失败: ${e.message}</td></tr>`;
|
||||
|
||||
+21
-13
@@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<header>
|
||||
<h1>币安 U本位合约 · 成交额排名</h1>
|
||||
<p class="subtitle">北京时间 08:00 切日 · Top30 · 高亮:≥1000万 USDT / |涨跌|≥5%</p>
|
||||
<p class="subtitle">北京时间 08:00 切日 · Top30 · 高亮:≥1000万 USDT / |涨跌|≥5% · 点击表头可排序</p>
|
||||
</header>
|
||||
|
||||
<section class="panel" id="panel-yesterday">
|
||||
@@ -17,16 +17,20 @@
|
||||
<h2>昨日周期</h2>
|
||||
<span class="period" id="yesterday-period">—</span>
|
||||
<span class="updated" id="yesterday-updated"></span>
|
||||
<div class="panel-actions">
|
||||
<button type="button" class="btn-secondary" data-export="yesterday">导出 CSV</button>
|
||||
<button type="button" class="btn-secondary" data-reset="yesterday">默认排序</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<table data-table="yesterday">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>合约</th>
|
||||
<th>成交额 (USDT)</th>
|
||||
<th>涨跌幅</th>
|
||||
<th>标记</th>
|
||||
<th class="sortable" data-sort="rank">排名</th>
|
||||
<th class="sortable" data-sort="symbol">合约</th>
|
||||
<th class="sortable" data-sort="quote_volume">成交额 (USDT)</th>
|
||||
<th class="sortable" data-sort="price_change_pct">涨跌幅</th>
|
||||
<th class="sortable" data-sort="tags">标记</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="yesterday-body"></tbody>
|
||||
@@ -39,16 +43,20 @@
|
||||
<h2>今日周期 <span class="live">实时</span></h2>
|
||||
<span class="period" id="today-period">—</span>
|
||||
<span class="updated" id="today-updated"></span>
|
||||
<div class="panel-actions">
|
||||
<button type="button" class="btn-secondary" data-export="today">导出 CSV</button>
|
||||
<button type="button" class="btn-secondary" data-reset="today">默认排序</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<table data-table="today">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>合约</th>
|
||||
<th>成交额 (USDT)</th>
|
||||
<th>涨跌幅</th>
|
||||
<th>标记</th>
|
||||
<th class="sortable" data-sort="rank">排名</th>
|
||||
<th class="sortable" data-sort="symbol">合约</th>
|
||||
<th class="sortable" data-sort="quote_volume">成交额 (USDT)</th>
|
||||
<th class="sortable" data-sort="price_change_pct">涨跌幅</th>
|
||||
<th class="sortable" data-sort="tags">标记</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="today-body"></tbody>
|
||||
|
||||
+51
-1
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user