diff --git a/reaction_board/web/static/router.js b/reaction_board/web/static/router.js
index 0bcc757..d58ad85 100644
--- a/reaction_board/web/static/router.js
+++ b/reaction_board/web/static/router.js
@@ -136,8 +136,11 @@ async function render(options = {}) {
if (scrollTop && !preserveScroll) {
window.scrollTo({ top: 0, behavior: "auto" });
}
- routeElement.innerHTML = `
`;
+ if (!preserveScroll) {
+ // 保留滚动位置的原地更新不要插入骨架屏,否则列表会短暂消失、滚动条闪烁。
+ routeElement.innerHTML = ``;
+ }
try {
await matched.route.view(routeElement, {
params: matched.match.slice(1),
diff --git a/reaction_board/web/static/views/rankings.js b/reaction_board/web/static/views/rankings.js
index bbd01e5..68f1d0b 100644
--- a/reaction_board/web/static/views/rankings.js
+++ b/reaction_board/web/static/views/rankings.js
@@ -16,6 +16,21 @@ const UP_SORTS = [
["count", "视频数"], ["views", "总播放"],
];
const METRIC_PRIMARY = new Set(["view", "like", "coin", "favorite", "share", "reply", "danmaku"]);
+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 controls({ scope, sort, within, growthDays, q, folder, minVideos, folders }) {
const sorts = scope === "up" ? UP_SORTS : VIDEO_SORTS;
@@ -87,7 +102,7 @@ export async function renderRankings(root, { query }) {
const minVideos = query.get("min_videos") || "1";
const page = Math.max(1, Number(query.get("page") || 1));
- const overview = await api("/api/overview");
+ const overview = await getOverview();
const folders = overview.folders || [];
const params = {