import { api } from "../api.js"; import { centerActiveChips, chip, emptyState, pager, upCard, videoCard, } from "../components.js"; import { escapeAttr, escapeHtml, formatCount, formatDate, formatNumber, formatPercent } from "../format.js"; import { replaceQuery } from "../router.js"; const VIDEO_SORTS = [ ["efficiency", "内容效率"], ["heat", "综合热度"], ["published", "发布时间"], ["added", "入库时间"], ["view", "播放"], ["coin", "投币"], ["favorite", "收藏"], ["reply", "评论"], ["interaction", "互动率"], ["growth", "播放增量"], ["growth_rate", "播放增速"], ]; const UP_SORTS = [ ["efficiency", "内容效率"], ["interaction_efficiency", "互动效率"], ["duration", "内容量"], ["views", "总播放"], ["count", "视频数"], ["avg_views", "平均播放"], ["interaction", "总互动"], ["latest", "最近发布"], ]; const TIME_RANGES = [ ["all", "全部时间"], ["7", "近 7 天"], ["30", "近 30 天"], ["90", "近 90 天"], ["365", "近 1 年"], ]; const VIDEO_METRIC_PRIMARY = new Set(["view", "coin", "favorite", "reply"]); const OVERVIEW_TTL = 60_000; let overviewCache = { at: 0, promise: null }; async function getOverview() { const now = Date.now(); if (!overviewCache.promise || now - overviewCache.at > OVERVIEW_TTL) { overviewCache = { at: now, promise: api("/api/overview") }; } try { return await overviewCache.promise; } catch (error) { overviewCache = { at: 0, promise: null }; throw error; } } function sortOptions(scope, sort) { const sorts = scope === "up" ? UP_SORTS : VIDEO_SORTS; return sorts.map(([value, label]) => ``).join(""); } function timeOptions(within, year, years) { const selected = year ? `year:${year}` : (within || "all"); const base = TIME_RANGES.map(([value, label]) => ``).join(""); const yearOptions = (years || []).map((value) => ``).join(""); return base + yearOptions; } function controls({ scope, sort, within, year, growthDays, q, years }) { return `
${scope === "video" && (sort === "growth" || sort === "growth_rate") ? `
增长区间 ${[[1, "1 天"], [7, "7 天"], [30, "30 天"]] .map(([value, label]) => chip(label, Number(growthDays) === value, "growth-days", value)).join("")}
` : ""} `; } function videoPrimary(item, sort) { if (sort === "efficiency") { return { primary: "efficiency_score", label: "播放/小时·收缩", text: formatCount(item.efficiency_score) }; } if (sort === "interaction_efficiency") { return { primary: "interaction_efficiency_score", label: "互动/小时·收缩", text: formatCount(item.interaction_efficiency_score) }; } if (sort === "interaction") { return { primary: "view", label: "互动率", text: formatPercent(item.interaction_rate) }; } if (VIDEO_METRIC_PRIMARY.has(sort)) { return { primary: sort, label: "", text: null }; } return { primary: "view", label: "播放", text: null }; } function upPrimary(item, sort) { if (sort === "efficiency") { return { label: "播放/小时", text: formatCount(item.efficiency) }; } if (sort === "interaction_efficiency") { return { label: "互动/小时", text: formatCount(item.interaction_efficiency) }; } if (sort === "duration") { return { label: "内容小时", text: item.content_hours === null ? "—" : `${formatNumber(item.content_hours)} 小时` }; } if (sort === "views") return { label: "总播放", text: formatCount(item.total_views) }; if (sort === "count") return { label: "视频数", text: formatNumber(item.video_count) }; if (sort === "avg_views") return { label: "平均播放", text: formatCount(item.avg_views) }; if (sort === "avg_interaction") return { label: "平均互动", text: formatPercent(item.avg_interaction) }; if (sort === "interaction") return { label: "总互动", text: formatCount(item.total_interactions) }; if (sort === "latest") return { label: "最近发布", text: formatDate(item.latest_pubdate) }; return { label: "播放/小时", text: formatCount(item.efficiency) }; } function upSecondary(item, sort) { // “总播放”排序时主指标已是总播放,副指标改为平均播放,避免重复。 if (sort === "views") { return { label: "平均播放", text: formatCount(item.avg_views) }; } return { label: "总播放", text: formatCount(item.total_views) }; } function listHtml(data, scope, sort, startRank) { if (!data.items.length) { return emptyState( data.available === false ? "当前榜单暂不可用" : (scope === "up" ? "没有匹配的 UP" : "没有匹配的视频"), data.reason || "换个筛选条件或清空搜索关键词试试。", ); } if (scope === "up") { return `
${data.items.map((item, index) => { const primary = upPrimary(item, sort); const secondary = upSecondary(item, sort); return upCard(item, { rank: startRank + index, primaryLabel: primary.label, primaryText: primary.text, secondaryLabel: secondary.label, secondaryText: secondary.text, }); }).join("")}
`; } return `
${data.items.map((item, index) => { const primary = videoPrimary(item, sort); return videoCard(item, { rank: startRank + index, primary: primary.primary, primaryLabel: primary.label, primaryText: primary.text, }); }).join("")}
`; } function durationNote(data) { const filter = data.duration_filter; if (!filter) return ""; const shrink = filter.shrink_minutes ? `并按 ${filter.shrink_minutes} 分钟的先验暴露量向全库均值收缩` : ""; return `
内容效率与互动效率仅统计时长 ≥ ${filter.min_duration_seconds} 秒、 发布满 ${filter.maturity_days} 天、播放 ≥ ${filter.min_view} 的视频${shrink};时长缺失的视频不参与排序。
`; } function bindControls(root) { root.querySelector("#rank-sort")?.addEventListener("change", (event) => { replaceQuery({ sort: event.target.value, page: null }); }); root.querySelector("#rank-time")?.addEventListener("change", (event) => { const value = event.target.value; if (value.startsWith("year:")) { replaceQuery({ year: value.slice(5), within_days: null, page: null }); } else if (value === "all") { replaceQuery({ year: null, within_days: null, page: null }); } else { replaceQuery({ year: null, within_days: value, page: null }); } }); root.querySelectorAll("[data-growth-days]").forEach((button) => button.addEventListener("click", () => { replaceQuery({ growth_days: button.dataset.growthDays, page: null }); })); root.querySelector("#rank-filter")?.addEventListener("submit", (event) => { event.preventDefault(); const value = new FormData(event.target).get("q") || ""; replaceQuery({ q: String(value).trim() || null, page: null }); }); root.querySelectorAll("[data-page]").forEach((button) => button.addEventListener("click", () => { if (button.disabled) return; replaceQuery({ page: button.dataset.page }); })); centerActiveChips(root); } async function renderScope(root, { query }, scope) { const sort = query.get("sort") || "efficiency"; const within = query.get("within_days") || ""; const year = query.get("year") || ""; const growthDays = query.get("growth_days") || "7"; const q = query.get("q") || ""; const page = Math.max(1, Number(query.get("page") || 1)); const overview = await getOverview(); const years = overview.years || []; const params = { sort, page, page_size: 20, within_days: within || null, year: year || null, growth_days: growthDays, q, }; let data; if (scope === "up") { data = await api("/api/ups", params); } else { data = await api("/api/videos", params); } const startRank = (data.page - 1) * data.page_size + 1; const title = scope === "up" ? "UP主" : "视频"; const unit = scope === "up" ? "个 UP" : "条视频"; root.innerHTML = `

${title} 共 ${formatCount(data.total)} ${unit}

${controls({ scope, sort, within, year, growthDays, q, years })} ${durationNote(data)} ${data.available === false ? `
${sort.startsWith("growth") ? "增长数据尚不可用" : "榜单筛选后为空"} ${escapeHtml(data.reason || "")}
` : ""} ${listHtml(data, scope, sort, startRank)} ${pager(data.page, data.pages)}
`; bindControls(root); } export function renderVideos(root, context) { return renderScope(root, context, "video"); } export function renderUps(root, context) { return renderScope(root, context, "up"); }