fix: 用 Zustand 缓存 Feed 列表,返回时恢复滚动且不重复请求

点击 Logo 仍强制刷新;后退忽略 history 上的 refreshFeed,并支持过期静默刷新与下拉刷新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 01:50:33 +08:00
parent 185649db7c
commit 132ca5c82c
8 changed files with 384 additions and 92 deletions

View File

@@ -0,0 +1,106 @@
import { create } from 'zustand';
import type { PostItem } from '../api/types';
import type { FeedSort } from '../components/FeedSortBar';
/** 列表缓存过期时间:超过后返回首页时静默刷新,仍先展示旧数据 */
export const FEED_STALE_MS = 5 * 60 * 1000;
/** 单条 Feed 筛选对应的缓存快照 */
export type FeedCacheEntry = {
posts: PostItem[];
postTotal: number;
page: number;
scrollTop: number;
/** 上次成功拉取 API 的时间戳ms */
lastFetchTime: number;
};
type FeedKeyParts = {
boardId: number;
keyword: string;
sort: FeedSort;
tag?: string;
author?: string;
titleOnly?: boolean;
};
/** 与筛选条件一一对应的缓存键 */
export function feedCacheKey(parts: FeedKeyParts): string {
const {
boardId,
keyword,
sort,
tag = '',
author = '',
titleOnly = false,
} = parts;
return `${boardId}:${keyword}:${tag}:${author}:${titleOnly ? 1 : 0}:${sort}`;
}
type HomeStoreState = {
/** 按筛选键存放多份列表,首页 / 板块 / 搜索互不覆盖 */
feeds: Record<string, FeedCacheEntry>;
getFeed: (key: string) => FeedCacheEntry | null;
setFeed: (key: string, data: Omit<FeedCacheEntry, 'lastFetchTime'> & { lastFetchTime?: number }) => void;
/** 滚动时只更新 scrollTop不改动列表数据 */
patchScroll: (key: string, scrollTop: number) => void;
isStale: (key: string, now?: number) => boolean;
clearFeed: (key: string) => void;
clearAll: () => void;
};
export const useHomeStore = create<HomeStoreState>((set, get) => ({
feeds: {},
getFeed: (key) => get().feeds[key] ?? null,
setFeed: (key, data) => {
set((state) => ({
feeds: {
...state.feeds,
[key]: {
posts: data.posts,
postTotal: data.postTotal,
page: data.page,
scrollTop: data.scrollTop,
lastFetchTime: data.lastFetchTime ?? Date.now(),
},
},
}));
},
patchScroll: (key, scrollTop) => {
const prev = get().feeds[key];
if (!prev) return;
// 数值未变则跳过,减少无意义的 store 更新
if (prev.scrollTop === scrollTop) return;
set((state) => ({
feeds: {
...state.feeds,
[key]: { ...prev, scrollTop },
},
}));
},
isStale: (key, now = Date.now()) => {
const entry = get().feeds[key];
if (!entry) return true;
return now - entry.lastFetchTime > FEED_STALE_MS;
},
clearFeed: (key) => {
set((state) => {
if (!(key in state.feeds)) return state;
const next = { ...state.feeds };
delete next[key];
return { feeds: next };
});
},
clearAll: () => set({ feeds: {} }),
}));
/** 非 React 路径(如 feedCache 封装)直接读写 */
export function getHomeStoreState() {
return useHomeStore.getState();
}