fix: 用 Zustand 缓存 Feed 列表,返回时恢复滚动且不重复请求
点击 Logo 仍强制刷新;后退忽略 history 上的 refreshFeed,并支持过期静默刷新与下拉刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
106
frontend/src/store/homeStore.ts
Normal file
106
frontend/src/store/homeStore.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user