Files
re-xxm/reaction_board/web/static/views/fans.js
T

123 lines
5.5 KiB
JavaScript

import { api } from "../api.js";
import { centerActiveChips, emptyState, sectionHeader } from "../components.js";
import { lineChart, themeColor } from "../charts.js";
import {
escapeAttr, escapeHtml, formatCount, formatDelta, formatNumber,
} from "../format.js";
import { replaceQuery } from "../router.js";
const RANGES = [["7", "7 天"], ["30", "30 天"], ["90", "90 天"], ["3650", "全部"]];
export async function renderFans(root, { query }) {
const days = query.get("days") || "90";
const mid = query.get("mid");
const start = query.get("start") || "";
const end = query.get("end") || "";
const customRange = Boolean(start || end);
const fans = await api("/api/fans", {
days: customRange ? null : days,
start,
end,
mid,
});
const options = fans.options || [];
root.innerHTML = `<section class="page fans-page">
<header class="page-head">
<div>
<h1>粉丝</h1>
<p class="page-sub">${escapeHtml(fans.name || "粉丝追踪")}</p>
</div>
${options.length > 1 ? `<select class="select" id="fan-mid">
${options.map((item) => `<option value="${item.mid}" ${String(fans.mid) === String(item.mid) ? "selected" : ""}>${escapeHtml(item.name)}</option>`).join("")}
</select>` : ""}
</header>
${!fans.available ? emptyState("暂无粉丝数据", "每日快照完成后会开始记录粉丝数。")
: `
<div class="hero-number">
<span>当前粉丝</span>
<strong>${formatCount(fans.follower)}</strong>
<small>${escapeHtml(fans.stat_date || "—")} · 首次记录 ${escapeHtml(fans.first_date || "—")}</small>
</div>
<div class="mini-stats">
<div><span>日增</span><strong class="${Number(fans.delta_1d) >= 0 ? "up" : "down"}">${fans.delta_1d === null ? "—" : formatDelta(fans.delta_1d)}</strong></div>
<div><span>7 日净增</span><strong class="${Number(fans.delta_7d) >= 0 ? "up" : "down"}">${fans.delta_7d === null ? "—" : formatDelta(fans.delta_7d)}</strong></div>
<div><span>累计增长(${escapeHtml(fans.first_date || "—")} 以来)</span><strong class="${Number(fans.delta_cum) >= 0 ? "up" : "down"}">${formatDelta(fans.delta_cum)}</strong></div>
</div>
<section class="panel chart-panel">
${sectionHeader("粉丝曲线")}
<div class="chip-row scroll-x">
${RANGES.map(([value, label]) => `<button type="button"
class="chip${!customRange && days === value ? " active" : ""}" data-days="${value}">${label}</button>`).join("")}
</div>
<form class="date-range" id="fan-range">
<input type="date" name="start" value="${escapeAttr(start)}" aria-label="开始日期">
<span>至</span>
<input type="date" name="end" value="${escapeAttr(end)}" aria-label="结束日期">
<button class="button" type="submit">应用</button>
${customRange ? `<button class="button ghost" type="button" id="fan-range-clear">清除</button>` : ""}
</form>
<div class="chart" id="fans-chart"></div>
<p class="section-note">
只绘制粉丝数,不叠加 reaction 视频数据;缺失日断开,不插值。
${fans.mode === "monthly"
? "当前范围超过一年,按每月粉丝峰值所在日期绘制。"
: `当前按日绘制(${escapeHtml(fans.range_start || "—")} ~ ${escapeHtml(fans.range_end || "—")})。`}
</p>
</section>
<section class="panel">
${sectionHeader("最近记录")}
<p class="section-note">默认展示最近 ${fans.recent_days || 14} 天;更早的数据只在曲线中显示。</p>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th>日期</th><th>粉丝</th><th>较上一成功日</th></tr></thead>
<tbody>
${(fans.recent_points || []).slice().reverse().map((point) => `
<tr class="${point.ok ? "" : "failed-row"}">
<td>${escapeHtml(point.stat_date)}</td>
<td>${point.ok ? formatNumber(point.follower) : "—"}</td>
<td>${point.delta === null || point.delta === undefined ? "—" : formatDelta(point.delta)}</td>
</tr>`).join("")}
</tbody>
</table>
</div>
</section>`}
</section>`;
root.querySelectorAll("[data-days]").forEach((button) => button.addEventListener("click", () => {
replaceQuery({ days: button.dataset.days, start: null, end: null });
}));
root.querySelector("#fan-mid")?.addEventListener("change", (event) => {
replaceQuery({ mid: event.target.value });
});
root.querySelector("#fan-range")?.addEventListener("submit", (event) => {
event.preventDefault();
const form = new FormData(event.target);
const nextStart = String(form.get("start") || "").trim();
const nextEnd = String(form.get("end") || "").trim();
if (nextStart && nextEnd && nextStart > nextEnd) {
window.alert("开始日期不能晚于结束日期");
return;
}
replaceQuery({ start: nextStart || null, end: nextEnd || null, days: null });
});
root.querySelector("#fan-range-clear")?.addEventListener("click", () => {
replaceQuery({ start: null, end: null, days: null });
});
centerActiveChips(root);
const chartElement = root.querySelector("#fans-chart");
if (chartElement) {
lineChart(chartElement, {
dates: fans.dates || [],
values: fans.series || [],
name: "粉丝",
color: themeColor("accent"),
scale: true,
});
}
}