109 lines
4.2 KiB
JavaScript
109 lines
4.2 KiB
JavaScript
import { api } from "../api.js";
|
|
import {
|
|
badge, emptyState, sectionHeader, videoCard,
|
|
} from "../components.js";
|
|
import { barChart, lineChart } from "../charts.js";
|
|
import {
|
|
escapeAttr, escapeHtml, formatCount, formatDate, formatPercent,
|
|
} from "../format.js";
|
|
import { replaceQuery } from "../router.js";
|
|
|
|
const SORTS = [
|
|
["view", "播放"], ["latest", "最新发布"], ["interaction", "互动率"],
|
|
];
|
|
|
|
function sortedVideos(videos, sort) {
|
|
const items = [...videos];
|
|
if (sort === "latest") {
|
|
items.sort((a, b) => b.pubdate - a.pubdate);
|
|
} else if (sort === "interaction") {
|
|
items.sort((a, b) => (b.interaction_rate || 0) - (a.interaction_rate || 0));
|
|
} else {
|
|
items.sort((a, b) => (b.metrics?.view || 0) - (a.metrics?.view || 0));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
export async function renderUp(root, { params, query }) {
|
|
const mid = Number(params[0]);
|
|
const sort = query.get("sort") || "view";
|
|
const up = await api(`/api/ups/${mid}`);
|
|
const videos = sortedVideos(up.videos || [], sort);
|
|
const topForBar = [...(up.videos || [])]
|
|
.sort((a, b) => (b.metrics?.view || 0) - (a.metrics?.view || 0))
|
|
.slice(0, 12);
|
|
|
|
root.innerHTML = `<section class="page up-page">
|
|
<header class="page-head">
|
|
<a class="back-link" href="#/rank?scope=up&sort=views">← 返回 UP 榜</a>
|
|
<a class="button ghost" href="${escapeAttr(up.space_url)}" target="_blank" rel="noreferrer">B 站空间</a>
|
|
</header>
|
|
|
|
<div class="detail-hero compact-hero">
|
|
<div class="detail-hero-body">
|
|
<h1>${escapeHtml(up.up_name || `UP ${mid}`)}</h1>
|
|
<div class="detail-meta">
|
|
<span>mid ${up.up_mid}</span>
|
|
${up.blacklisted ? badge("黑名单 · 仅影响发现", "warning") : ""}
|
|
${(up.folders || []).map((title) => badge(title)).join("")}
|
|
</div>
|
|
<p class="section-note">首次发布 ${escapeHtml(formatDate(up.first_pubdate))} · 最近发布 ${escapeHtml(formatDate(up.latest_pubdate))}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mini-stats up-stats">
|
|
<div><span>视频数</span><strong>${formatCount(up.video_count)}</strong></div>
|
|
<div><span>总播放</span><strong>${formatCount(up.total_views)}</strong></div>
|
|
<div><span>平均播放</span><strong>${formatCount(up.avg_views)}</strong></div>
|
|
<div><span>总互动</span><strong>${formatCount(up.total_interactions)}</strong></div>
|
|
<div><span>平均互动率</span><strong>${formatPercent(up.avg_interaction)}</strong></div>
|
|
</div>
|
|
|
|
<section class="panel chart-panel">
|
|
${sectionHeader("该 UP 视频总播放趋势")}
|
|
${up.series?.dates?.length ? `<div class="chart" id="up-trend"></div>` : emptyState("暂无趋势", "该 UP 还没有成功快照。")}
|
|
<p class="section-note">按该 UP 全部视频的每日播放合计绘制;缺失日期断开。</p>
|
|
</section>
|
|
|
|
<section class="panel chart-panel">
|
|
${sectionHeader("单视频播放对比")}
|
|
${topForBar.length ? `<div class="chart chart-tall" id="up-bars"></div>` : emptyState("暂无视频数据")}
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="section-head">
|
|
<h2>视频列表</h2>
|
|
<select class="select" id="up-sort">
|
|
${SORTS.map(([value, label]) => `<option value="${value}" ${sort === value ? "selected" : ""}>${label}</option>`).join("")}
|
|
</select>
|
|
</div>
|
|
<div class="video-list">
|
|
${videos.length ? videos.map((item) => videoCard(item)).join("") : emptyState("没有视频")}
|
|
</div>
|
|
</section>
|
|
</section>`;
|
|
|
|
const trendElement = root.querySelector("#up-trend");
|
|
if (trendElement) {
|
|
lineChart(trendElement, {
|
|
dates: up.series.dates,
|
|
values: up.series.views,
|
|
name: "总播放",
|
|
color: "#fb7299",
|
|
scale: true,
|
|
});
|
|
}
|
|
const barsElement = root.querySelector("#up-bars");
|
|
if (barsElement) {
|
|
barChart(barsElement, {
|
|
labels: topForBar.map((item) => (item.title || `#${item.aid}`).slice(0, 12)),
|
|
values: topForBar.map((item) => item.metrics?.view || 0),
|
|
name: "播放",
|
|
color: "#00aeec",
|
|
});
|
|
}
|
|
root.querySelector("#up-sort")?.addEventListener("change", (event) => {
|
|
replaceQuery({ sort: event.target.value });
|
|
});
|
|
}
|