修改企业微信推送

This commit is contained in:
dekun
2026-05-26 10:45:27 +08:00
parent 10dc45198c
commit 74bd241579
3 changed files with 74 additions and 132 deletions
+49 -109
View File
@@ -13,7 +13,7 @@ const tableState = {
const PERIOD_LS_PREFIX = "ba_period_"; const PERIOD_LS_PREFIX = "ba_period_";
const PERIOD_TTL_MS = 4 * 60 * 60 * 1000; const PERIOD_TTL_MS = 4 * 60 * 60 * 1000;
let statsData = null; let wecomPreviewData = null;
let currentView = "today"; let currentView = "today";
const SORT_KEYS = { const SORT_KEYS = {
rank: (r) => Number(r.rank) || 0, rank: (r) => Number(r.rank) || 0,
@@ -262,58 +262,25 @@ function downloadCsv(name, header, rows, periodStart) {
a.click(); a.click();
} }
function renderStatsTable() { function updateStatsHeader(payload) {
const wrap = document.getElementById("stats-table-wrap"); const criteria = document.getElementById("stats-criteria");
if (!wrap || !statsData) return; const summary = document.getElementById("stats-summary");
const desc = document.getElementById("stats-desc");
const items = statsData.items || []; if (!payload) return;
document.getElementById("stats-criteria").textContent = statsData.criteria || ""; if (criteria) {
document.getElementById("stats-desc").textContent = statsData.message || ""; criteria.textContent = payload.ok
const sum = statsData.summary; ? payload.period_label || "—"
document.getElementById("stats-summary").textContent = statsData.ok : "数据未就绪";
? `符合条件 ${statsData.count} 个 · 三日交集 ${sum?.intersection ?? 0}`
: "数据未就绪";
if (!statsData.ok) {
wrap.innerHTML = `<p class="loading">${statsData.message || "请等待三个周期数据就绪"}</p>`;
return;
} }
if (summary) {
if (!items.length) { summary.textContent = payload.ok
wrap.innerHTML = '<p class="loading">暂无符合条件的合约</p>'; ? `${payload.count} 个币种` +
return; (payload.parts > 1 ? ` · 企微将分 ${payload.parts} 条发送` : "")
: "";
}
if (desc && payload.message && !payload.ok) {
desc.textContent = payload.message;
} }
wrap.innerHTML = `
<table data-table="stats" class="stats-table">
<thead><tr>
<th>合约</th>
<th>今日排名</th><th>今日涨跌</th><th>今日成交额</th>
<th>昨日排名</th><th>昨日涨跌</th><th>昨日成交额</th>
<th>前日排名</th><th>前日涨跌</th><th>前日成交额</th>
<th>三日总成交额</th>
</tr></thead>
<tbody id="stats-body"></tbody>
</table>`;
const body = document.getElementById("stats-body");
body.innerHTML = items
.map((row) => {
const d = (p) => row[p] || {};
const cell = (p, f) => {
const x = d(p);
const pct = x.price_change_pct ?? 0;
return `<td class="${f === "pct" ? pctClass(pct) : ""}">${f === "pct" ? x.price_change_pct_fmt || "—" : f === "rank" ? x.rank ?? "—" : x.quote_volume_fmt || "—"}</td>`;
};
return `<tr class="row-highlight stats-row" data-symbol="${row.symbol}">
<td><strong>${row.symbol}</strong></td>
${cell("today", "rank")}${cell("today", "pct")}${cell("today", "vol")}
${cell("yesterday", "rank")}${cell("yesterday", "pct")}${cell("yesterday", "vol")}
${cell("daybefore", "rank")}${cell("daybefore", "pct")}${cell("daybefore", "vol")}
<td class="stats-total-vol">${formatVol(row.total_quote_volume)}</td>
</tr>`;
})
.join("");
} }
function escapeHtml(s) { function escapeHtml(s) {
@@ -324,59 +291,43 @@ function escapeHtml(s) {
.replace(/\n/g, "<br>"); .replace(/\n/g, "<br>");
} }
function formatVol(v) {
if (v >= 1e8) return (v / 1e8).toFixed(2) + "亿";
if (v >= 1e4) return (v / 1e4).toFixed(2) + "万";
return String(Math.round(v));
}
function renderWecomDayRow(label, row) {
if (!row?.rank) {
return `<div class="wecom-day muted"><span class="wecom-day-label">${label}</span>—</div>`;
}
const pct = row.price_change_pct ?? 0;
return `<div class="wecom-day">
<span class="wecom-day-label">${label}</span>
<span class="wecom-rank-num">#${row.rank}</span>
<span class="wecom-vol">${row.quote_volume_fmt || row.quote_volume}</span>
<span class="${pctClass(pct)}">${row.price_change_pct_fmt || pct.toFixed(2) + "%"}</span>
<span class="wecom-fr">${row.funding_rate_fmt || "—"}</span>
</div>`;
}
function renderWecomPreview(payload) { function renderWecomPreview(payload) {
const panel = document.getElementById("wecom-preview-panel");
const cards = document.getElementById("wecom-preview-cards"); const cards = document.getElementById("wecom-preview-cards");
const meta = document.getElementById("wecom-preview-meta"); const meta = document.getElementById("wecom-preview-meta");
if (!panel || !cards) return; if (!cards) return;
panel.classList.remove("hidden"); wecomPreviewData = payload;
updateStatsHeader(payload);
if (!payload?.ok) { if (!payload?.ok) {
if (meta) meta.textContent = "未就绪"; if (meta) meta.textContent = "";
cards.innerHTML = `<p class="error">${escapeHtml(payload?.message || "无法生成预览")}</p>`; cards.innerHTML = `<p class="error">${escapeHtml(payload?.message || "无法生成预览")}</p>`;
return; return;
} }
if (meta) { if (meta) {
const partsHint = meta.textContent =
payload.parts > 1 ? ` · 企微分 ${payload.parts} 条发送` : ""; `昨日周期 ${payload.period_label || "—"} · 昨/今/前 = 排名+涨跌幅` +
meta.textContent = `${payload.period_label || "—"} · ${payload.count} 个币种${partsHint}`; (payload.parts > 1 ? ` · 超长将分 ${payload.parts} 条企微消息` : "");
} }
if (!payload.items?.length) { if (!payload.items?.length) {
cards.innerHTML = '<p class="loading">暂无三日交集币种</p>'; cards.innerHTML = '<p class="loading">暂无三日交集币种</p>';
return; return;
} }
cards.innerHTML = payload.items cards.innerHTML = payload.items
.map( .map((it) => {
(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}<strong>#${row.rank}</strong><span class="${pctClass(pct)}">${pctStr}</span>`;
};
return `
<article class="wecom-card"> <article class="wecom-card">
<header class="wecom-card-head"> <header class="wecom-card-head">
<span class="wecom-seq">${it.rank}</span> <span class="wecom-seq">${it.rank}</span>
<strong class="wecom-symbol">${it.symbol}</strong> <strong class="wecom-symbol">${it.symbol}</strong>
</header> </header>
${renderWecomDayRow("昨", it.yesterday)} <p class="wecom-compact-line">${line("昨 ", it.yesterday)} ${line("今 ", it.today)} ${line("前 ", it.daybefore)}</p>
${renderWecomDayRow("今日", it.today)} </article>`;
${renderWecomDayRow("前日", it.daybefore)} })
</article>`
)
.join(""); .join("");
} }
@@ -417,35 +368,25 @@ async function testWecomPush() {
} }
async function loadStats() { async function loadStats() {
document.getElementById("stats-table-wrap").innerHTML = await loadWecomPreview();
'<p class="loading">统计中…</p>';
try {
const res = await fetch("/api/stats/three-day");
statsData = await res.json();
renderStatsTable();
await loadWecomPreview();
} catch (e) {
document.getElementById("stats-table-wrap").innerHTML = `<p class="error">${e.message}</p>`;
}
} }
function exportStatsCsv() { function exportStatsCsv() {
if (!statsData?.items?.length) return alert("暂无数据"); const items = wecomPreviewData?.items;
if (!items?.length) return alert("暂无数据");
const header = [ const header = [
"合约", "合约",
"今日排名", "今日涨跌%", "今日成交额", "今日排名", "今日涨跌%",
"昨日排名", "昨日涨跌%", "昨日成交额", "昨日排名", "昨日涨跌%",
"前日排名", "前日涨跌%", "前日成交额", "前日排名", "前日涨跌%",
"三日总成交额",
]; ];
const rows = statsData.items.map((r) => [ const rows = items.map((r) => [
r.symbol, r.symbol,
r.today?.rank, r.today?.price_change_pct, r.today?.quote_volume, r.today?.rank, r.today?.price_change_pct,
r.yesterday?.rank, r.yesterday?.price_change_pct, r.yesterday?.quote_volume, r.yesterday?.rank, r.yesterday?.price_change_pct,
r.daybefore?.rank, r.daybefore?.price_change_pct, r.daybefore?.quote_volume, r.daybefore?.rank, r.daybefore?.price_change_pct,
r.total_quote_volume,
]); ]);
downloadCsv("binance-three-day-stats", header, rows, "stats"); downloadCsv("binance-wecom-push", header, rows, wecomPreviewData?.period_label);
} }
function switchView(view) { function switchView(view) {
@@ -458,7 +399,7 @@ function switchView(view) {
}); });
if (view === "stats") { if (view === "stats") {
if (!statsData) loadStats(); if (!wecomPreviewData) loadStats();
return; return;
} }
@@ -492,11 +433,10 @@ document.getElementById("btn-refresh").addEventListener("click", async () => {
}); });
document.getElementById("btn-reload-stats")?.addEventListener("click", () => { document.getElementById("btn-reload-stats")?.addEventListener("click", () => {
statsData = null; wecomPreviewData = null;
loadStats(); loadStats();
}); });
document.getElementById("btn-export-stats")?.addEventListener("click", exportStatsCsv); document.getElementById("btn-export-stats")?.addEventListener("click", exportStatsCsv);
document.getElementById("btn-push-preview")?.addEventListener("click", loadWecomPreview);
document.getElementById("btn-push-test")?.addEventListener("click", testWecomPush); document.getElementById("btn-push-test")?.addEventListener("click", testWecomPush);
loadPeriod("today"); loadPeriod("today");
+9 -10
View File
@@ -16,7 +16,7 @@
<button type="button" class="nav-item active" data-view="today">今日周期</button> <button type="button" class="nav-item active" data-view="today">今日周期</button>
<button type="button" class="nav-item" data-view="yesterday">昨日周期</button> <button type="button" class="nav-item" data-view="yesterday">昨日周期</button>
<button type="button" class="nav-item" data-view="daybefore">前日周期</button> <button type="button" class="nav-item" data-view="daybefore">前日周期</button>
<button type="button" class="nav-item" data-view="stats">数据统计</button> <button type="button" class="nav-item" data-view="stats">企微推送</button>
</nav> </nav>
<main id="view-today" class="view-panel active"> <main id="view-today" class="view-panel active">
@@ -67,23 +67,22 @@
<main id="view-stats" class="view-panel"> <main id="view-stats" class="view-panel">
<section class="panel"> <section class="panel">
<div class="panel-head"> <div class="panel-head">
<h2>数据统计</h2> <h2>企微推送预览</h2>
<span class="period" id="stats-criteria"></span> <span class="period" id="stats-criteria"></span>
<span class="updated" id="stats-summary"></span> <span class="updated" id="stats-summary"></span>
<div class="panel-actions"> <div class="panel-actions">
<button type="button" class="btn-secondary" id="btn-reload-stats">重新统计</button> <button type="button" class="btn-secondary" id="btn-reload-stats">刷新预览</button>
<button type="button" class="btn-secondary" id="btn-export-stats">导出 CSV</button> <button type="button" class="btn-secondary" id="btn-export-stats">导出 CSV</button>
<button type="button" class="btn-secondary" id="btn-push-preview">预览企微推送</button>
<button type="button" class="btn-secondary" id="btn-push-test">测试推送企微</button> <button type="button" class="btn-secondary" id="btn-push-test">测试推送企微</button>
</div> </div>
</div> </div>
<p class="stats-desc" id="stats-desc"></p> <p class="stats-desc" id="stats-desc">三日 Top30 交集 · 与发到企业微信的内容一致</p>
<section class="wecom-preview-panel hidden" id="wecom-preview-panel"> <section class="wecom-preview-panel" id="wecom-preview-panel">
<h3>企微推送预览 <span class="wecom-preview-meta" id="wecom-preview-meta"></span></h3> <p class="wecom-preview-meta-line" id="wecom-preview-meta"></p>
<p class="stats-desc">仅包含「三日 Top30 交集」币种;企微为紧凑单行/币,超过 4096 字自动分多条发送。</p> <div id="wecom-preview-cards" class="wecom-preview-cards">
<div id="wecom-preview-cards" class="wecom-preview-cards"></div> <p class="loading">加载预览…</p>
</div>
</section> </section>
<div class="table-wrap" id="stats-table-wrap"></div>
</section> </section>
</main> </main>
+16 -13
View File
@@ -367,26 +367,29 @@ button:hover {
} }
.wecom-preview-panel { .wecom-preview-panel {
margin: 1rem 0 0; margin-top: 0.5rem;
padding: 1rem; padding: 1rem;
background: #121a26; background: #121a26;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
} }
.wecom-preview-panel.hidden { .wecom-preview-meta-line {
display: none; margin: 0 0 0.75rem;
} font-size: 0.85rem;
.wecom-preview-panel h3 {
margin: 0 0 0.5rem;
font-size: 1rem;
}
.wecom-preview-meta {
font-size: 0.8rem;
color: var(--muted); color: var(--muted);
font-weight: normal; }
.wecom-compact-line {
margin: 0;
font-size: 0.84rem;
line-height: 1.6;
color: var(--text);
}
.wecom-compact-line strong {
margin: 0 0.15rem;
color: var(--accent);
} }
.wecom-preview-cards { .wecom-preview-cards {