${line("昨 ", it.yesterday)} ${line("今 ", it.today)} ${line("前 ", it.daybefore)}
const PERIOD_API = {
today: "/api/today/top30",
yesterday: "/api/yesterday/top30",
daybefore: "/api/daybefore/top30",
};
const tableState = {
today: { items: [], meta: {}, sortKey: "rank", sortDir: "asc" },
yesterday: { items: [], meta: {}, sortKey: "rank", sortDir: "asc" },
daybefore: { items: [], meta: {}, sortKey: "rank", sortDir: "asc" },
};
const PERIOD_LS_PREFIX = "ba_period_";
const PERIOD_TTL_MS = 4 * 60 * 60 * 1000;
let wecomPreviewData = null;
let currentView = "today";
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,
funding_rate: (r) => Number(r.funding_rate_pct) || 0,
tags: (r) => {
let s = 0;
if (r.is_high_volume) s += 2;
if (r.is_high_change) s += 1;
return s;
},
total_quote_volume: (r) => Number(r.total_quote_volume) || 0,
avg_change_pct: (r) => Number(r.avg_change_pct) || 0,
};
const TABLE_HEADER = `
`;
function formatPeriod(start, end) {
const fmt = (s) => (s || "").replace("T", " ").slice(0, 16);
return `${fmt(start)} ~ ${fmt(end)}`;
}
function tagText(row) {
const t = [];
if (row.is_high_volume) t.push("千万+");
if (row.is_high_change) t.push("涨跌5%+");
return t.join(" ") || "";
}
function renderTags(row) {
const p = [];
if (row.is_high_volume) p.push('千万+');
if (row.is_high_change) p.push('涨跌5%+');
return p.length ? p.join("") : "—";
}
function pctClass(pct) {
if (pct > 0) return "pct-up";
if (pct < 0) return "pct-down";
return "";
}
function sortItems(items, key, dir, customKeys) {
const getter = (customKeys || SORT_KEYS)[key] || SORT_KEYS.rank;
return [...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;
});
}
function ensurePeriodTable(periodId) {
const wrap = document.getElementById(`${periodId}-table-wrap`);
if (!wrap) return null;
let table = wrap.querySelector(`table[data-table="${periodId}"]`);
if (!table) {
wrap.innerHTML = `排名
合约
日线图
成交额 (USDT)
涨跌幅
资金费率
标记
${escapeHtml(payload?.message || "无法生成预览")}
`; return; } if (meta) { meta.textContent = `昨日周期 ${payload.period_label || "—"} · 昨/今/前 = 排名+涨跌幅` + (payload.parts > 1 ? ` · 超长将分 ${payload.parts} 条企微消息` : ""); } if (!payload.items?.length) { cards.innerHTML = '暂无三日交集币种
'; return; } cards.innerHTML = payload.items .map((it) => { const line = (label, row) => { if (!row?.rank) return `${label}—`; const pct = row.price_change_pct ?? 0; const pctStr = row.price_change_pct_fmt || `${pct.toFixed(2)}%`; return `${label}#${row.rank}${pctStr}`; }; return `${line("昨 ", it.yesterday)} ${line("今 ", it.today)} ${line("前 ", it.daybefore)}
生成预览…
'; try { const res = await fetch("/api/push/preview"); const data = await res.json(); renderWecomPreview(data); } catch (e) { if (cards) cards.innerHTML = `${e.message}
`; } } async function testWecomPush() { const el = document.getElementById("push-status"); if (el) el.textContent = "推送中…"; try { const res = await fetch("/api/push/test", { method: "POST" }); const data = await res.json(); if (!res.ok) { const detail = data.detail; const msg = typeof detail === "string" ? detail : Array.isArray(detail) ? detail.map((x) => x.msg).join("; ") : data.message || res.statusText; throw new Error(msg); } if (el) el.textContent = data.message || "推送成功"; await loadWecomPreview(); } catch (e) { if (el) el.textContent = "推送失败"; alert(e.message); } } async function loadStats() { await loadWecomPreview(); } function exportStatsCsv() { const items = wecomPreviewData?.items; if (!items?.length) return alert("暂无数据"); const header = [ "合约", "今日排名", "今日涨跌%", "昨日排名", "昨日涨跌%", "前日排名", "前日涨跌%", ]; const rows = items.map((r) => [ r.symbol, r.today?.rank, r.today?.price_change_pct, r.yesterday?.rank, r.yesterday?.price_change_pct, r.daybefore?.rank, r.daybefore?.price_change_pct, ]); downloadCsv("binance-wecom-push", header, rows, wecomPreviewData?.period_label); } function switchView(view) { currentView = view; document.querySelectorAll(".nav-item").forEach((b) => { b.classList.toggle("active", b.dataset.view === view); }); document.querySelectorAll(".view-panel").forEach((p) => { p.classList.toggle("active", p.id === `view-${view}`); }); if (view === "stats") { if (!wecomPreviewData) loadStats(); return; } const tbody = document.getElementById(`${view}-body`); if (tbody && tableState[view].items.length) { renderPeriodTable(view); enqueueCharts(tbody); if (typeof enqueueFundingCharts === "function") enqueueFundingCharts(tbody); } else if (!tableState[view].items.length) { loadPeriod(view); } } document.getElementById("main-nav").addEventListener("click", (e) => { const btn = e.target.closest(".nav-item"); if (btn?.dataset.view) switchView(btn.dataset.view); }); document.querySelectorAll("[data-export]").forEach((btn) => { btn.addEventListener("click", () => exportPeriodCsv(btn.dataset.export)); }); document.querySelectorAll("[data-reset]").forEach((btn) => { btn.addEventListener("click", () => resetSort(btn.dataset.reset)); }); document.getElementById("btn-refresh").addEventListener("click", async () => { document.getElementById("status").textContent = "刷新中…"; await fetch("/api/refresh/today", { method: "POST" }); await loadPeriod("today", true); if (currentView === "stats") await loadStats(); }); document.getElementById("btn-reload-stats")?.addEventListener("click", () => { wecomPreviewData = null; loadStats(); }); document.getElementById("btn-export-stats")?.addEventListener("click", exportStatsCsv); document.getElementById("btn-push-test")?.addEventListener("click", testWecomPush); loadPeriod("today"); loadPeriod("yesterday"); loadPeriod("daybefore");