import { api } from "../api.js"; import { badge, cover, emptyState, metricChips, metricGrid, sectionHeader, videoCard, } from "../components.js"; import { lineChart, emptyChart } from "../charts.js"; import { escapeAttr, escapeHtml, formatCount, formatDate, formatDelta, formatDuration, formatPercent, formatScore, metricLabel, statusLabel, } from "../format.js"; import { replaceQuery } from "../router.js"; const METRICS = ["view", "like", "coin", "favorite", "share", "reply", "danmaku"]; const RANGES = [["7", "7 天"], ["30", "30 天"], ["90", "90 天"], ["all", "全部"]]; function snapshotTable(points) { if (!points.length) return emptyState("暂无快照", "该视频还没有成功采集的数据。"); return `
${METRICS.map((metric) => ``).join("")} ${points.slice().reverse().map((point) => ` ${METRICS.map((metric) => ``).join("")} `).join("")}
日期${metricLabel(metric)}状态
${escapeHtml(point.stat_date)}${point.ok ? formatCount(point[metric]) : "—"}${point.ok ? "成功" : `失败`}
`; } export async function renderVideo(root, { params, query }) { const aid = Number(params[0]); const metric = METRICS.includes(query.get("metric")) ? query.get("metric") : "view"; const range = query.get("range") || "30"; const trendDays = range === "all" ? "all" : range; const [detail, trend] = await Promise.all([ api(`/api/videos/${aid}`), api(`/api/videos/${aid}/trend`, { range: trendDays }), ]); const up = detail.up_mid ? await api(`/api/ups/${detail.up_mid}`) : null; const samples = detail.snapshot_count || 0; const others = up?.videos?.filter((item) => item.aid !== aid).slice(0, 5) || []; root.innerHTML = `
← 返回榜单
${detail.url ? `打开 B 站` : ""}
${cover(detail, "hero-cover")}

${escapeHtml(detail.title)}

${detail.up_mid ? `${escapeHtml(detail.up_name || "未知 UP")}` : escapeHtml(detail.up_name || "未知 UP")} ${escapeHtml(formatDate(detail.pubdate))} ${escapeHtml(formatDuration(detail.duration))} ${detail.folder_title ? badge(detail.folder_title) : ""} ${badge(statusLabel(detail.status), `status-${detail.status}`)} ${samples < 2 ? badge("样本不足", "warning") : ""}
${detail.blacklisted ? `

该 UP 在黑名单中;黑名单只影响未来发现,不影响当前视频追踪。

` : ""}
${metricGrid(detail)}
互动率${formatPercent(detail.interaction_rate)}
综合热度${formatScore(detail.heat)}
全库百分位${detail.heat_percentile === null || detail.heat_percentile === undefined ? "—" : `前 ${Math.max(0, 100 - detail.heat_percentile).toFixed(0)}%`}
播放日增${detail.growth?.available ? formatDelta(detail.growth.delta) : "—"}
快照数${samples}
最近快照${escapeHtml(detail.snapshot_date || "—")}
${sectionHeader("七项趋势")}
${metricChips(METRICS, metric)}
${RANGES.map(([value, label]) => ``).join("")}
${trend.available ? `
` : emptyState("暂无趋势数据", "该视频还没有成功快照。")} ${!trend.available ? "" : `

缺失日期会断开,不插值;失败快照按缺失处理。

`}
${sectionHeader("快照记录")} ${snapshotTable(trend.points || [])}
${up ? `

该 UP 的其他视频

查看 UP 页 →
${others.length ? others.map((item) => videoCard(item)).join("") : emptyState("没有其他视频")}
` : ""}
`; root.querySelectorAll("[data-metric]").forEach((button) => button.addEventListener("click", () => { replaceQuery({ metric: button.dataset.metric }); })); root.querySelectorAll("[data-range]").forEach((button) => button.addEventListener("click", () => { replaceQuery({ range: button.dataset.range }); })); const chartElement = root.querySelector("#video-trend"); if (chartElement) { lineChart(chartElement, { dates: trend.dates || [], values: trend.series?.[metric] || [], name: metricLabel(metric), color: metric === "view" ? "#fb7299" : "#00aeec", scale: true, }); } }