feat(web): dropdown sort and time filters with year support
This commit is contained in:
1 parent
bc3bef0743
commit
9be7d1cab2
10 files changed
+191
-67
No files matched your search
@@ -102,7 +102,7 @@ export async function renderHome(root) {
|
||||
<section class="panel">
|
||||
<div class="section-head">
|
||||
<h2>最近发布</h2>
|
||||
<a class="text-link" href="/videos?sort=published&within_days=14">查看全部</a>
|
||||
<a class="text-link" href="/videos?sort=published&within_days=30">查看全部</a>
|
||||
</div>
|
||||
<p class="section-note">按视频发布时间排序,与入库时间无关。</p>
|
||||
<div class="video-list">
|
||||
|
||||
@@ -8,6 +8,8 @@ import { replaceQuery } from "../router.js";
|
||||
const VIDEO_SORTS = [
|
||||
["efficiency", "内容效率"],
|
||||
["heat", "综合热度"],
|
||||
["published", "发布时间"],
|
||||
["added", "入库时间"],
|
||||
["view", "播放"],
|
||||
["coin", "投币"],
|
||||
["favorite", "收藏"],
|
||||
@@ -26,6 +28,13 @@ const UP_SORTS = [
|
||||
["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 };
|
||||
@@ -43,8 +52,22 @@ async function getOverview() {
|
||||
}
|
||||
}
|
||||
|
||||
function controls({ scope, sort, within, growthDays, q, folder, minVideos, folders }) {
|
||||
function sortOptions(scope, sort) {
|
||||
const sorts = scope === "up" ? UP_SORTS : VIDEO_SORTS;
|
||||
return sorts.map(([value, label]) =>
|
||||
`<option value="${value}" ${value === sort ? "selected" : ""}>${escapeHtml(label)}</option>`).join("");
|
||||
}
|
||||
|
||||
function timeOptions(within, year, years) {
|
||||
const selected = year ? `year:${year}` : (within || "all");
|
||||
const base = TIME_RANGES.map(([value, label]) =>
|
||||
`<option value="${value}" ${selected === value ? "selected" : ""}>${escapeHtml(label)}</option>`).join("");
|
||||
const yearOptions = (years || []).map((value) =>
|
||||
`<option value="year:${value}" ${selected === `year:${value}` ? "selected" : ""}>${value} 年</option>`).join("");
|
||||
return base + yearOptions;
|
||||
}
|
||||
|
||||
function controls({ scope, sort, within, year, growthDays, q, minVideos, years }) {
|
||||
return `
|
||||
<div class="rank-toolbar">
|
||||
<form class="rank-search" id="rank-filter">
|
||||
@@ -52,25 +75,19 @@ function controls({ scope, sort, within, growthDays, q, folder, minVideos, folde
|
||||
placeholder="${scope === "up" ? "搜索 UP 名" : "搜索标题或 UP"}" autocomplete="off">
|
||||
<button class="button" type="submit">搜索</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="chip-row scroll-x">
|
||||
${sorts.map(([value, label]) => chip(label, sort === value, "sort", value)).join("")}
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="chip-row scroll-x">
|
||||
${[["all", "全部发布时间"], ["7", "近 7 天"], ["30", "近 30 天"], ["90", "近 90 天"]]
|
||||
.map(([value, label]) => chip(label, String(within || "all") === value, "within", value)).join("")}
|
||||
</div>
|
||||
${scope === "video" && folders.length ? `<select class="select" id="folder-filter">
|
||||
<option value="">全部收藏夹</option>
|
||||
${folders.map((item) => `<option value="${item.media_id}"
|
||||
${String(folder) === String(item.media_id) ? "selected" : ""}>${escapeHtml(item.title || item.year)}</option>`).join("")}
|
||||
</select>` : ""}
|
||||
${scope === "up" ? `<select class="select" id="min-videos">
|
||||
<option value="1" ${String(minVideos) === "1" ? "selected" : ""}>全部 UP</option>
|
||||
<option value="2" ${String(minVideos) === "2" ? "selected" : ""}>≥ 2 条视频</option>
|
||||
<option value="5" ${String(minVideos) === "5" ? "selected" : ""}>≥ 5 条视频</option>
|
||||
</select>` : ""}
|
||||
<label class="select-label">排序
|
||||
<select class="select" id="rank-sort">${sortOptions(scope, sort)}</select>
|
||||
</label>
|
||||
<label class="select-label">时间
|
||||
<select class="select" id="rank-time">${timeOptions(within, year, years)}</select>
|
||||
</label>
|
||||
${scope === "up" ? `<label class="select-label">视频数
|
||||
<select class="select" id="min-videos">
|
||||
<option value="1" ${String(minVideos) === "1" ? "selected" : ""}>全部 UP</option>
|
||||
<option value="2" ${String(minVideos) === "2" ? "selected" : ""}>≥ 2 条</option>
|
||||
<option value="5" ${String(minVideos) === "5" ? "selected" : ""}>≥ 5 条</option>
|
||||
</select>
|
||||
</label>` : ""}
|
||||
</div>
|
||||
${scope === "video" && (sort === "growth" || sort === "growth_rate") ? `
|
||||
<div class="chip-row growth-days">
|
||||
@@ -150,19 +167,23 @@ function durationNote(data) {
|
||||
发布满 ${filter.maturity_days} 天、播放 ≥ ${filter.min_view} 的视频${shrink};时长缺失的视频不参与排序。</div>`;
|
||||
}
|
||||
|
||||
function bindControls(root, scope) {
|
||||
root.querySelectorAll("[data-sort]").forEach((button) => button.addEventListener("click", () => {
|
||||
replaceQuery({ sort: button.dataset.sort, page: null });
|
||||
}));
|
||||
root.querySelectorAll("[data-within]").forEach((button) => button.addEventListener("click", () => {
|
||||
replaceQuery({ within_days: button.dataset.within === "all" ? null : button.dataset.within, page: null });
|
||||
}));
|
||||
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("#folder-filter")?.addEventListener("change", (event) => {
|
||||
replaceQuery({ folder: event.target.value || null, page: null });
|
||||
});
|
||||
root.querySelector("#min-videos")?.addEventListener("change", (event) => {
|
||||
replaceQuery({ min_videos: event.target.value, page: null });
|
||||
});
|
||||
@@ -179,23 +200,23 @@ function bindControls(root, scope) {
|
||||
}
|
||||
|
||||
async function renderScope(root, { query }, scope) {
|
||||
const defaultSort = "efficiency";
|
||||
const sort = query.get("sort") || defaultSort;
|
||||
const within = query.get("within_days") || "all";
|
||||
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 folder = query.get("folder") || "";
|
||||
const minVideos = query.get("min_videos") || "1";
|
||||
const page = Math.max(1, Number(query.get("page") || 1));
|
||||
|
||||
const overview = await getOverview();
|
||||
const folders = overview.folders || [];
|
||||
const years = overview.years || [];
|
||||
|
||||
const params = {
|
||||
sort,
|
||||
page,
|
||||
page_size: 20,
|
||||
within_days: within === "all" ? null : within,
|
||||
within_days: within || null,
|
||||
year: year || null,
|
||||
growth_days: growthDays,
|
||||
q,
|
||||
};
|
||||
@@ -203,7 +224,7 @@ async function renderScope(root, { query }, scope) {
|
||||
if (scope === "up") {
|
||||
data = await api("/api/ups", { ...params, min_videos: minVideos });
|
||||
} else {
|
||||
data = await api("/api/videos", { ...params, folder: folder || null });
|
||||
data = await api("/api/videos", params);
|
||||
}
|
||||
|
||||
const startRank = (data.page - 1) * data.page_size + 1;
|
||||
@@ -214,7 +235,7 @@ async function renderScope(root, { query }, scope) {
|
||||
<header class="page-head">
|
||||
<h1>${title} <span class="page-sub">共 ${formatCount(data.total)} ${unit}</span></h1>
|
||||
</header>
|
||||
${controls({ scope, sort, within, growthDays, q, folder, minVideos, folders })}
|
||||
${controls({ scope, sort, within, year, growthDays, q, minVideos, years })}
|
||||
${durationNote(data)}
|
||||
${data.available === false ? `<div class="notice-row warning">
|
||||
<strong>${sort.startsWith("growth") ? "增长数据尚不可用" : "榜单筛选后为空"}</strong>
|
||||
@@ -224,7 +245,7 @@ async function renderScope(root, { query }, scope) {
|
||||
${pager(data.page, data.pages)}
|
||||
</section>`;
|
||||
|
||||
bindControls(root, scope);
|
||||
bindControls(root);
|
||||
}
|
||||
|
||||
export function renderVideos(root, context) {
|
||||
@@ -233,4 +254,4 @@ export function renderVideos(root, context) {
|
||||
|
||||
export function renderUps(root, context) {
|
||||
return renderScope(root, context, "up");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user