refactor: 优化分页存储架构和缓存逻辑...
This commit is contained in:
@@ -37,10 +37,12 @@ const apiRequest = async (url, options = {}) => {
|
||||
|
||||
export const eventService = {
|
||||
getEvents: (params = {}) => {
|
||||
// Filter out empty params
|
||||
const cleanParams = Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null && v !== ''));
|
||||
// Filter out null, undefined, and empty strings (but keep 0 and false)
|
||||
const cleanParams = Object.fromEntries(
|
||||
Object.entries(params).filter(([_, v]) => v !== null && v !== undefined && v !== '')
|
||||
);
|
||||
const query = new URLSearchParams(cleanParams).toString();
|
||||
return apiRequest(`/api/events/?${query}`);
|
||||
return apiRequest(`/api/events?${query}`);
|
||||
},
|
||||
getHotEvents: (params = {}) => {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
|
||||
@@ -222,10 +222,18 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
total: response.data.pagination?.total || 0,
|
||||
per_page: finalPerPage
|
||||
});
|
||||
// 【兜底处理】支持多种 pagination 字段名:pages (后端) / total_pages (旧Mock) / totalPages
|
||||
const paginationData = response.data.pagination || {};
|
||||
const calculatedTotalPages = paginationData.pages // ← 后端格式 (优先)
|
||||
|| paginationData.total_pages // ← Mock 旧格式 (兼容)
|
||||
|| paginationData.totalPages // ← 其他可能格式
|
||||
|| Math.ceil((paginationData.total || 0) / finalPerPage); // ← 兜底计算
|
||||
|
||||
return {
|
||||
mode,
|
||||
events: response.data.events,
|
||||
total: response.data.pagination?.total || 0,
|
||||
total: paginationData.total || 0,
|
||||
totalPages: calculatedTotalPages,
|
||||
page,
|
||||
per_page: finalPerPage,
|
||||
clearCache,
|
||||
@@ -234,10 +242,15 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
}
|
||||
|
||||
logger.warn('CommunityData', '动态新闻返回数据为空', response);
|
||||
// 【兜底处理】空数据情况也尝试读取 pagination
|
||||
const emptyPaginationData = response.data?.pagination || {};
|
||||
const emptyTotalPages = emptyPaginationData.pages || emptyPaginationData.total_pages || 0;
|
||||
|
||||
return {
|
||||
mode,
|
||||
events: [],
|
||||
total: 0,
|
||||
totalPages: emptyTotalPages,
|
||||
page,
|
||||
per_page: finalPerPage,
|
||||
clearCache,
|
||||
@@ -327,12 +340,22 @@ const communityDataSlice = createSlice({
|
||||
hotEvents: [],
|
||||
|
||||
// 【纵向模式】独立存储(传统分页 + 每页10条)
|
||||
verticalEvents: [], // 完整缓存列表(累积所有已加载数据)
|
||||
verticalTotal: 0, // 服务端总数量(用于计算总页数)
|
||||
verticalEventsByPage: {}, // 页码映射存储 { 1: [10条], 2: [8条], 3: [10条] }
|
||||
verticalPagination: { // 分页元数据
|
||||
total: 0, // 总记录数
|
||||
total_pages: 0, // 总页数
|
||||
current_page: 1, // 当前页码
|
||||
per_page: 10 // 每页大小
|
||||
},
|
||||
|
||||
// 【平铺模式】独立存储(虚拟滚动 + 每页30条)
|
||||
fourRowEvents: [], // 完整缓存列表(虚拟滚动的数据源)
|
||||
fourRowTotal: 0, // 服务端总数量(用于判断 hasMore)
|
||||
fourRowPagination: { // 分页元数据
|
||||
total: 0, // 总记录数
|
||||
total_pages: 0, // 总页数
|
||||
current_page: 1, // 当前页码
|
||||
per_page: 30 // 每页大小
|
||||
},
|
||||
|
||||
eventFollowStatus: {}, // 事件关注状态(全局共享){ [eventId]: { isFollowing: boolean, followerCount: number } }
|
||||
|
||||
@@ -367,10 +390,10 @@ const communityDataSlice = createSlice({
|
||||
state.hotEvents = [];
|
||||
|
||||
// 清除动态新闻数据(两个模式)
|
||||
state.verticalEvents = [];
|
||||
state.verticalEventsByPage = {};
|
||||
state.fourRowEvents = [];
|
||||
state.verticalTotal = 0;
|
||||
state.fourRowTotal = 0;
|
||||
state.verticalPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 10 };
|
||||
state.fourRowPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 30 };
|
||||
|
||||
logger.info('CommunityData', '所有缓存已清除');
|
||||
},
|
||||
@@ -392,13 +415,13 @@ const communityDataSlice = createSlice({
|
||||
logger.info('CommunityData', '热点事件缓存已清除');
|
||||
} else if (type === 'verticalEvents') {
|
||||
// verticalEvents 不使用 localStorage,只清除 Redux state
|
||||
state.verticalEvents = [];
|
||||
state.verticalTotal = 0;
|
||||
state.verticalEventsByPage = {};
|
||||
state.verticalPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 10 };
|
||||
logger.info('CommunityData', '纵向模式事件数据已清除');
|
||||
} else if (type === 'fourRowEvents') {
|
||||
// fourRowEvents 不使用 localStorage,只清除 Redux state
|
||||
state.fourRowEvents = [];
|
||||
state.fourRowTotal = 0;
|
||||
state.fourRowPagination = { total: 0, total_pages: 0, current_page: 1, per_page: 30 };
|
||||
logger.info('CommunityData', '平铺模式事件数据已清除');
|
||||
}
|
||||
},
|
||||
@@ -438,7 +461,7 @@ const communityDataSlice = createSlice({
|
||||
state.error[stateKey] = null;
|
||||
})
|
||||
.addCase(fetchDynamicNews.fulfilled, (state, action) => {
|
||||
const { mode, events, total, page, clearCache, prependMode, isEmpty } = action.payload;
|
||||
const { mode, events, total, page, per_page, clearCache, prependMode, isEmpty } = action.payload;
|
||||
const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents';
|
||||
const totalKey = mode === 'four-row' ? 'fourRowTotal' : 'verticalTotal';
|
||||
|
||||
@@ -459,63 +482,103 @@ const communityDataSlice = createSlice({
|
||||
page,
|
||||
clearCache,
|
||||
prependMode,
|
||||
'state[stateKey] 之前': state[stateKey].length,
|
||||
'state[stateKey] 类型': Array.isArray(state[stateKey]) ? 'Array' : 'Object',
|
||||
'state[stateKey] 之前': Array.isArray(state[stateKey])
|
||||
? `数组长度: ${state[stateKey].length}`
|
||||
: `对象页数: ${Object.keys(state[stateKey] || {}).length}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* 【数据去重和追加逻辑】
|
||||
* 【数据存储逻辑】根据模式选择不同的存储策略
|
||||
*
|
||||
* 三种模式:
|
||||
* 1. clearCache 模式:直接替换(用于刷新或模式切换)
|
||||
* 2. prependMode 模式:去重后插入头部(用于定时刷新,获取最新事件)
|
||||
* 3. append 模式(默认):去重后追加到末尾(用于无限滚动加载下一页)
|
||||
* 纵向模式(vertical):页码映射存储
|
||||
* - clearCache=true: 清空所有页,存储新页(第1页专用)
|
||||
* - clearCache=false: 存储到对应页码(第2、3、4...页)
|
||||
* - 优点:每页独立,不受去重影响,支持缓存
|
||||
*
|
||||
* 去重逻辑(append 和 prepend 模式):
|
||||
* - 使用 Set 提取已存在的事件 ID
|
||||
* - 过滤掉新数据中与现有数据重复的事件
|
||||
* - 只保留真正的新事件
|
||||
*
|
||||
* 为什么需要去重:
|
||||
* 1. 网络请求乱序:例如第3页比第2页先返回
|
||||
* 2. 定时刷新冲突:用户正在浏览时后台刷新了第一页
|
||||
* 3. 后端分页漂移:新事件插入导致页码边界变化
|
||||
* 平铺模式(four-row):去重追加存储
|
||||
* - clearCache=true: 直接替换(用于刷新)
|
||||
* - prependMode=true: 去重后插入头部(定时刷新)
|
||||
* - 默认:去重后追加到末尾(无限滚动)
|
||||
* - 优点:累积显示,支持虚拟滚动
|
||||
*/
|
||||
if (mode === 'vertical') {
|
||||
// 【纵向模式】页码映射存储
|
||||
if (clearCache) {
|
||||
// 清空缓存模式:直接替换
|
||||
state[stateKey] = events;
|
||||
logger.debug('CommunityData', `清空缓存并加载新数据 (${mode})`, {
|
||||
// 第1页:清空所有页,只保留新页
|
||||
state.verticalEventsByPage = { [page]: events };
|
||||
logger.debug('CommunityData', `清空缓存并加载第${page}页 (纵向模式)`, {
|
||||
count: events.length
|
||||
});
|
||||
|
||||
// 🔍 调试:清空缓存后的状态
|
||||
console.log('%c[Redux] clearCache 模式,直接替换数据', 'color: #10B981; font-weight: bold;', {
|
||||
'state[stateKey] 之后': state[stateKey].length
|
||||
console.log('%c[Redux] 纵向模式 clearCache,清空所有页', 'color: #10B981; font-weight: bold;', {
|
||||
page,
|
||||
eventsCount: events.length
|
||||
});
|
||||
} else {
|
||||
// 其他页:存储到对应页码
|
||||
state.verticalEventsByPage = state.verticalEventsByPage || {};
|
||||
state.verticalEventsByPage[page] = events;
|
||||
logger.debug('CommunityData', `存储第${page}页数据 (纵向模式)`, {
|
||||
page,
|
||||
count: events.length,
|
||||
totalPages: Object.keys(state.verticalEventsByPage || {}).length
|
||||
});
|
||||
console.log('%c[Redux] 纵向模式追加页面', 'color: #10B981; font-weight: bold;', {
|
||||
page,
|
||||
eventsCount: events.length,
|
||||
cachedPages: Object.keys(state.verticalEventsByPage || {})
|
||||
});
|
||||
}
|
||||
} else if (mode === 'four-row') {
|
||||
// 【平铺模式】去重追加存储
|
||||
if (clearCache) {
|
||||
// 清空缓存模式:直接替换
|
||||
state.fourRowEvents = events;
|
||||
logger.debug('CommunityData', `清空缓存并加载新数据 (平铺模式)`, {
|
||||
count: events.length
|
||||
});
|
||||
console.log('%c[Redux] 平铺模式 clearCache,直接替换数据', 'color: #10B981; font-weight: bold;', {
|
||||
eventsCount: events.length
|
||||
});
|
||||
} else if (prependMode) {
|
||||
// 追加到头部模式(用于定时刷新):去重后插入头部
|
||||
const existingIds = new Set(state[stateKey].map(e => e.id));
|
||||
const existingIds = new Set((state.fourRowEvents || []).map(e => e.id));
|
||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||
state[stateKey] = [...newEvents, ...state[stateKey]];
|
||||
logger.debug('CommunityData', `追加新数据到头部 (${mode})`, {
|
||||
state.fourRowEvents = [...newEvents, ...(state.fourRowEvents || [])];
|
||||
logger.debug('CommunityData', `追加新数据到头部 (平铺模式)`, {
|
||||
newCount: newEvents.length,
|
||||
totalCount: state[stateKey].length
|
||||
totalCount: state.fourRowEvents.length
|
||||
});
|
||||
} else {
|
||||
// 默认追加模式:去重后追加到末尾(用于虚拟滚动加载下一页)
|
||||
const existingIds = new Set(state[stateKey].map(e => e.id));
|
||||
const existingIds = new Set((state.fourRowEvents || []).map(e => e.id));
|
||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||
state[stateKey] = [...state[stateKey], ...newEvents];
|
||||
state.fourRowEvents = [...(state.fourRowEvents || []), ...newEvents];
|
||||
|
||||
logger.debug('CommunityData', `追加新数据(去重,${mode})`, {
|
||||
logger.debug('CommunityData', `追加新数据(去重,平铺模式)`, {
|
||||
page,
|
||||
originalEventsCount: events.length,
|
||||
newEventsCount: newEvents.length,
|
||||
filteredCount: events.length - newEvents.length,
|
||||
totalCount: state[stateKey].length
|
||||
totalCount: state.fourRowEvents.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
state[totalKey] = total;
|
||||
// 【元数据存储】存储完整的 pagination 对象
|
||||
const paginationKey = mode === 'four-row' ? 'fourRowPagination' : 'verticalPagination';
|
||||
const finalPerPage = per_page || (mode === 'four-row' ? 30 : 10); // 兜底默认值
|
||||
state[paginationKey] = {
|
||||
total: total,
|
||||
total_pages: action.payload.totalPages || Math.ceil(total / finalPerPage),
|
||||
current_page: page,
|
||||
per_page: finalPerPage
|
||||
};
|
||||
|
||||
console.log('%c[Redux] 更新分页元数据', 'color: #8B5CF6; font-weight: bold;', {
|
||||
mode,
|
||||
pagination: state[paginationKey]
|
||||
});
|
||||
|
||||
state.loading[stateKey] = false;
|
||||
})
|
||||
@@ -550,14 +613,18 @@ export const selectLoading = (state) => state.communityData.loading;
|
||||
export const selectError = (state) => state.communityData.error;
|
||||
|
||||
// 纵向模式数据选择器
|
||||
export const selectVerticalEvents = (state) => state.communityData.verticalEvents;
|
||||
export const selectVerticalTotal = (state) => state.communityData.verticalTotal;
|
||||
export const selectVerticalCachedCount = (state) => state.communityData.verticalEvents.length;
|
||||
export const selectVerticalEventsByPage = (state) => state.communityData.verticalEventsByPage;
|
||||
export const selectVerticalPagination = (state) => state.communityData.verticalPagination;
|
||||
export const selectVerticalCachedPageCount = (state) => Object.keys(state.communityData.verticalEventsByPage || {}).length;
|
||||
|
||||
// 平铺模式数据选择器
|
||||
export const selectFourRowEvents = (state) => state.communityData.fourRowEvents;
|
||||
export const selectFourRowTotal = (state) => state.communityData.fourRowTotal;
|
||||
export const selectFourRowCachedCount = (state) => state.communityData.fourRowEvents.length;
|
||||
export const selectFourRowPagination = (state) => state.communityData.fourRowPagination;
|
||||
export const selectFourRowCachedCount = (state) => (state.communityData.fourRowEvents || []).length;
|
||||
|
||||
// 向后兼容的选择器(已废弃,建议使用 selectVerticalPagination.total)
|
||||
export const selectVerticalTotal = (state) => state.communityData.verticalPagination?.total || 0;
|
||||
export const selectFourRowTotal = (state) => state.communityData.fourRowPagination?.total || 0;
|
||||
|
||||
// 组合选择器
|
||||
export const selectPopularKeywordsWithLoading = (state) => ({
|
||||
@@ -574,11 +641,12 @@ export const selectHotEventsWithLoading = (state) => ({
|
||||
|
||||
// 纵向模式数据 + 加载状态选择器
|
||||
export const selectVerticalEventsWithLoading = (state) => ({
|
||||
data: state.communityData.verticalEvents, // 完整缓存列表
|
||||
data: state.communityData.verticalEventsByPage, // 页码映射 { 1: [...], 2: [...] }
|
||||
loading: state.communityData.loading.verticalEvents,
|
||||
error: state.communityData.error.verticalEvents,
|
||||
total: state.communityData.verticalTotal, // 服务端总数量
|
||||
cachedCount: state.communityData.verticalEvents.length // 已缓存有效数量
|
||||
pagination: state.communityData.verticalPagination, // 完整分页元数据 { total, total_pages, current_page, per_page }
|
||||
total: state.communityData.verticalPagination?.total || 0, // 向后兼容:服务端总数量
|
||||
cachedPageCount: Object.keys(state.communityData.verticalEventsByPage || {}).length // 已缓存页数
|
||||
});
|
||||
|
||||
// 平铺模式数据 + 加载状态选择器
|
||||
@@ -586,8 +654,9 @@ export const selectFourRowEventsWithLoading = (state) => ({
|
||||
data: state.communityData.fourRowEvents, // 完整缓存列表
|
||||
loading: state.communityData.loading.fourRowEvents,
|
||||
error: state.communityData.error.fourRowEvents,
|
||||
total: state.communityData.fourRowTotal, // 服务端总数量
|
||||
cachedCount: state.communityData.fourRowEvents.length // 已缓存有效数量
|
||||
pagination: state.communityData.fourRowPagination, // 完整分页元数据 { total, total_pages, current_page, per_page }
|
||||
total: state.communityData.fourRowPagination?.total || 0, // 向后兼容:服务端总数量
|
||||
cachedCount: (state.communityData.fourRowEvents || []).length // 已缓存有效数量
|
||||
});
|
||||
|
||||
export default communityDataSlice.reducer;
|
||||
|
||||
@@ -82,36 +82,48 @@ const DynamicNewsCard = forwardRef(({
|
||||
// 🔍 调试:从 Redux 读取数据
|
||||
console.log('%c[DynamicNewsCard] 从 Redux 读取数据', 'color: #3B82F6; font-weight: bold;', {
|
||||
currentMode,
|
||||
'verticalData.data?.length': verticalData.data?.length || 0,
|
||||
'verticalData.data type': typeof verticalData.data,
|
||||
'verticalData.data keys': verticalData.data ? Object.keys(verticalData.data) : [],
|
||||
'verticalData.total': verticalData.total,
|
||||
'verticalData.cachedCount': verticalData.cachedCount,
|
||||
'verticalData.cachedPageCount': verticalData.cachedPageCount,
|
||||
'verticalData.loading': verticalData.loading,
|
||||
'fourRowData.data?.length': fourRowData.data?.length || 0,
|
||||
'fourRowData.total': fourRowData.total,
|
||||
});
|
||||
|
||||
// 根据模式选择数据源(添加默认值避免解构失败)
|
||||
// 根据模式选择数据源
|
||||
// 纵向模式:data 是页码映射 { 1: [...], 2: [...] }
|
||||
// 平铺模式:data 是数组 [...]
|
||||
const modeData = currentMode === 'four-row' ? fourRowData : verticalData;
|
||||
const {
|
||||
data: allCachedEvents = [],
|
||||
data = currentMode === 'vertical' ? {} : [], // 纵向是对象,平铺是数组
|
||||
loading = false,
|
||||
error = null,
|
||||
total = 0,
|
||||
cachedCount = 0
|
||||
} = currentMode === 'four-row' ? fourRowData : verticalData;
|
||||
pagination, // 分页元数据
|
||||
total = 0, // 向后兼容
|
||||
cachedCount = 0,
|
||||
cachedPageCount = 0
|
||||
} = modeData;
|
||||
|
||||
// 传递给 usePagination 的数据
|
||||
const allCachedEventsByPage = currentMode === 'vertical' ? data : undefined;
|
||||
const allCachedEvents = currentMode === 'four-row' ? data : undefined;
|
||||
|
||||
// 🔍 调试:选择的数据源
|
||||
console.log('%c[DynamicNewsCard] 选择的数据源', 'color: #3B82F6; font-weight: bold;', {
|
||||
mode: currentMode,
|
||||
'allCachedEvents.length': allCachedEvents.length,
|
||||
'allCachedEventsByPage': allCachedEventsByPage ? Object.keys(allCachedEventsByPage) : 'undefined',
|
||||
'allCachedEvents?.length': allCachedEvents?.length,
|
||||
total,
|
||||
cachedCount,
|
||||
cachedPageCount,
|
||||
loading,
|
||||
error
|
||||
});
|
||||
|
||||
// 🔍 调试:记录每次渲染
|
||||
dynamicNewsCardRenderCount++;
|
||||
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents.length}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
|
||||
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents?.length || 0}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
|
||||
|
||||
// 关注按钮点击处理
|
||||
const handleToggleFollow = useCallback((eventId) => {
|
||||
@@ -141,15 +153,16 @@ const DynamicNewsCard = forwardRef(({
|
||||
totalPages,
|
||||
hasMore,
|
||||
currentPageEvents,
|
||||
displayEvents, // 新增:累积显示的事件列表
|
||||
isAccumulateMode, // 新增:是否累积模式
|
||||
displayEvents, // 当前显示的事件列表
|
||||
handlePageChange,
|
||||
handleModeToggle,
|
||||
loadNextPage, // 新增:加载下一页
|
||||
loadPrevPage // 新增:加载上一页
|
||||
loadNextPage, // 加载下一页
|
||||
loadPrevPage // 加载上一页
|
||||
} = usePagination({
|
||||
allCachedEvents,
|
||||
total,
|
||||
allCachedEventsByPage, // 纵向模式:页码映射
|
||||
allCachedEvents, // 平铺模式:数组
|
||||
pagination, // 分页元数据对象
|
||||
total, // 向后兼容
|
||||
cachedCount,
|
||||
dispatch,
|
||||
toast,
|
||||
@@ -183,7 +196,11 @@ const DynamicNewsCard = forwardRef(({
|
||||
|
||||
// 初始加载 - 只在组件首次挂载且对应模式数据为空时执行
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && allCachedEvents.length === 0) {
|
||||
const isDataEmpty = currentMode === 'vertical'
|
||||
? Object.keys(allCachedEventsByPage || {}).length === 0
|
||||
: (allCachedEvents?.length || 0) === 0;
|
||||
|
||||
if (!hasInitialized.current && isDataEmpty) {
|
||||
hasInitialized.current = true;
|
||||
dispatch(fetchDynamicNews({
|
||||
mode: mode, // 传递当前模式
|
||||
@@ -194,7 +211,7 @@ const DynamicNewsCard = forwardRef(({
|
||||
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
|
||||
}));
|
||||
}
|
||||
}, [dispatch, allCachedEvents.length, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发
|
||||
}, [dispatch, allCachedEventsByPage, allCachedEvents, currentMode, mode, pageSize]); // ✅ 移除 filters 依赖,避免重复触发
|
||||
|
||||
// 监听筛选条件变化 - 清空缓存并重新请求数据
|
||||
useEffect(() => {
|
||||
@@ -231,7 +248,11 @@ const DynamicNewsCard = forwardRef(({
|
||||
|
||||
// 监听模式切换 - 如果新模式数据为空,请求数据
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current && allCachedEvents.length === 0) {
|
||||
const isDataEmpty = currentMode === 'vertical'
|
||||
? Object.keys(allCachedEventsByPage || {}).length === 0
|
||||
: (allCachedEvents?.length || 0) === 0;
|
||||
|
||||
if (hasInitialized.current && isDataEmpty) {
|
||||
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
|
||||
dispatch(fetchDynamicNews({
|
||||
mode: mode,
|
||||
@@ -309,11 +330,10 @@ const DynamicNewsCard = forwardRef(({
|
||||
{currentPageEvents && currentPageEvents.length > 0 ? (
|
||||
<EventScrollList
|
||||
events={currentPageEvents}
|
||||
displayEvents={displayEvents} // 新增:累积显示的事件列表
|
||||
isAccumulateMode={isAccumulateMode} // 新增:是否累积模式
|
||||
loadNextPage={loadNextPage} // 新增:加载下一页
|
||||
loadPrevPage={loadPrevPage} // 新增:加载上一页
|
||||
onFourRowEventClick={handleFourRowEventClick} // 新增:四排模式事件点击
|
||||
displayEvents={displayEvents} // 累积显示的事件列表(平铺模式)
|
||||
loadNextPage={loadNextPage} // 加载下一页
|
||||
loadPrevPage={loadPrevPage} // 加载上一页
|
||||
onFourRowEventClick={handleFourRowEventClick} // 四排模式事件点击
|
||||
selectedEvent={selectedEvent}
|
||||
onEventSelect={setSelectedEvent}
|
||||
borderColor={borderColor}
|
||||
|
||||
@@ -33,7 +33,6 @@ import VerticalModeLayout from './VerticalModeLayout';
|
||||
const EventScrollList = ({
|
||||
events,
|
||||
displayEvents, // 累积显示的事件列表(四排模式用)
|
||||
isAccumulateMode, // 是否累积模式
|
||||
loadNextPage, // 加载下一页(无限滚动)
|
||||
loadPrevPage, // 加载上一页(双向无限滚动)
|
||||
onFourRowEventClick, // 四排模式事件点击回调(打开弹窗)
|
||||
|
||||
@@ -14,23 +14,31 @@ import {
|
||||
/**
|
||||
* 分页逻辑自定义 Hook
|
||||
* @param {Object} options - Hook 配置选项
|
||||
* @param {Array} options.allCachedEvents - 完整缓存事件列表
|
||||
* @param {number} options.total - 服务端总数量
|
||||
* @param {Object} options.allCachedEventsByPage - 纵向模式页码映射 { 1: [...], 2: [...] }
|
||||
* @param {Array} options.allCachedEvents - 平铺模式数组 [...]
|
||||
* @param {Object} options.pagination - 分页元数据 { total, total_pages, current_page, per_page }
|
||||
* @param {number} options.total - 【废弃】服务端总数量(向后兼容,建议使用 pagination.total)
|
||||
* @param {number} options.cachedCount - 已缓存数量
|
||||
* @param {Function} options.dispatch - Redux dispatch 函数
|
||||
* @param {Function} options.toast - Toast 通知函数
|
||||
* @param {Object} options.filters - 筛选条件
|
||||
* @returns {Object} 分页状态和方法
|
||||
*/
|
||||
export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, toast, filters = {} }) => {
|
||||
export const usePagination = ({
|
||||
allCachedEventsByPage, // 纵向模式:页码映射
|
||||
allCachedEvents, // 平铺模式:数组
|
||||
pagination, // 分页元数据对象
|
||||
total, // 向后兼容
|
||||
cachedCount,
|
||||
dispatch,
|
||||
toast,
|
||||
filters = {}
|
||||
}) => {
|
||||
// 本地状态
|
||||
const [currentPage, setCurrentPage] = useState(PAGINATION_CONFIG.INITIAL_PAGE);
|
||||
const [loadingPage, setLoadingPage] = useState(null);
|
||||
const [mode, setMode] = useState(DEFAULT_MODE);
|
||||
|
||||
// 累积显示的事件列表(用于四排模式的无限滚动)
|
||||
const [accumulatedEvents, setAccumulatedEvents] = useState([]);
|
||||
|
||||
// 根据模式决定每页显示数量
|
||||
const pageSize = (() => {
|
||||
switch (mode) {
|
||||
@@ -43,92 +51,55 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
}
|
||||
})();
|
||||
|
||||
// 计算总页数(基于服务端总数据量)
|
||||
const totalPages = Math.ceil(total / pageSize) || 1;
|
||||
// 【优化】优先使用后端返回的 total_pages,避免前端重复计算
|
||||
// 向后兼容:如果没有 pagination 对象,则使用 total 计算
|
||||
const totalPages = pagination?.total_pages || Math.ceil((pagination?.total || total || 0) / pageSize) || 1;
|
||||
|
||||
// 检查是否还有更多数据
|
||||
const hasMore = cachedCount < total;
|
||||
// 检查是否还有更多数据(使用页码判断,不受去重影响)
|
||||
const hasMore = currentPage < totalPages;
|
||||
|
||||
// 判断是否使用累积模式(四排模式 + 纵向模式)
|
||||
const isAccumulateMode = mode === DISPLAY_MODES.FOUR_ROW || mode === DISPLAY_MODES.VERTICAL;
|
||||
|
||||
// 从缓存中切片获取当前页数据(过滤 null 占位符)
|
||||
// 从页码映射或数组获取当前页数据
|
||||
const currentPageEvents = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return allCachedEvents.slice(startIndex, endIndex).filter(event => event !== null);
|
||||
}, [allCachedEvents, currentPage, pageSize]);
|
||||
|
||||
// 当前显示的事件列表(累积模式 vs 分页模式)
|
||||
const displayEvents = useMemo(() => {
|
||||
if (isAccumulateMode) {
|
||||
// 四排模式:累积显示所有已加载的事件
|
||||
return accumulatedEvents;
|
||||
if (mode === DISPLAY_MODES.VERTICAL) {
|
||||
// 纵向模式:从页码映射获取当前页
|
||||
return allCachedEventsByPage?.[currentPage] || [];
|
||||
} else {
|
||||
// 其他模式:只显示当前页
|
||||
// 平铺模式:返回全部累积数据
|
||||
return allCachedEvents || [];
|
||||
}
|
||||
}, [mode, allCachedEventsByPage, allCachedEvents, currentPage]);
|
||||
|
||||
// 当前显示的事件列表
|
||||
const displayEvents = useMemo(() => {
|
||||
if (mode === DISPLAY_MODES.FOUR_ROW) {
|
||||
// 平铺模式:返回全部累积数据
|
||||
return allCachedEvents || [];
|
||||
} else {
|
||||
// 纵向模式:返回当前页数据
|
||||
return currentPageEvents;
|
||||
}
|
||||
}, [isAccumulateMode, accumulatedEvents, currentPageEvents]);
|
||||
|
||||
/**
|
||||
* 子函数1: 检查目标页缓存状态
|
||||
* @param {number} targetPage - 目标页码
|
||||
* @returns {Object} { isTargetPageCached, targetPageInfo }
|
||||
*/
|
||||
const checkTargetPageCache = useCallback((targetPage) => {
|
||||
const targetPageStartIndex = (targetPage - 1) * pageSize;
|
||||
const targetPageEndIndex = targetPageStartIndex + pageSize;
|
||||
const targetPageData = allCachedEvents.slice(targetPageStartIndex, targetPageEndIndex);
|
||||
const validTargetData = targetPageData.filter(e => e !== null);
|
||||
// 修复:确保 expectedCount 不为负数
|
||||
// - 当 total = 0 时,expectedCount = pageSize,强制发起请求
|
||||
// - 当 total - targetPageStartIndex < 0 时,expectedCount = 0
|
||||
const expectedCount = total === 0 ? pageSize : Math.max(0, Math.min(pageSize, total - targetPageStartIndex));
|
||||
const isTargetPageCached = validTargetData.length >= expectedCount;
|
||||
|
||||
logger.debug('DynamicNewsCard', '目标页缓存检查', {
|
||||
targetPage,
|
||||
targetPageStartIndex,
|
||||
targetPageEndIndex,
|
||||
targetPageDataLength: targetPageData.length,
|
||||
validTargetDataLength: validTargetData.length,
|
||||
expectedCount,
|
||||
isTargetPageCached
|
||||
});
|
||||
|
||||
return {
|
||||
isTargetPageCached,
|
||||
targetPageInfo: {
|
||||
startIndex: targetPageStartIndex,
|
||||
endIndex: targetPageEndIndex,
|
||||
validCount: validTargetData.length,
|
||||
expectedCount
|
||||
}
|
||||
};
|
||||
}, [allCachedEvents, pageSize, total]);
|
||||
|
||||
// 已删除: calculatePreloadRange(不再需要预加载)
|
||||
|
||||
// 已删除: findMissingPages(不再需要查找缺失页面)
|
||||
}, [mode, allCachedEvents, currentPageEvents]);
|
||||
|
||||
/**
|
||||
* 加载单个页面数据
|
||||
* @param {number} targetPage - 目标页码
|
||||
* @param {boolean} clearCache - 是否清空缓存(第1页专用)
|
||||
* @returns {Promise<boolean>} 是否加载成功
|
||||
*/
|
||||
const loadPage = useCallback(async (targetPage) => {
|
||||
const loadPage = useCallback(async (targetPage, clearCache = false) => {
|
||||
// 显示 loading 状态
|
||||
setLoadingPage(targetPage);
|
||||
|
||||
try {
|
||||
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
|
||||
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}`, 'color: #16A34A;');
|
||||
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}, clearCache=${clearCache}`, 'color: #16A34A;');
|
||||
console.log(`%c 筛选条件:`, 'color: #16A34A;', filters);
|
||||
|
||||
logger.debug('DynamicNewsCard', '开始加载页面数据', {
|
||||
targetPage,
|
||||
pageSize,
|
||||
mode,
|
||||
clearCache,
|
||||
filters
|
||||
});
|
||||
|
||||
@@ -138,7 +109,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
page: targetPage,
|
||||
per_page: pageSize,
|
||||
pageSize,
|
||||
clearCache: false,
|
||||
clearCache,
|
||||
filters
|
||||
});
|
||||
|
||||
@@ -146,7 +117,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
mode: mode, // 传递 mode 参数
|
||||
per_page: pageSize,
|
||||
pageSize: pageSize,
|
||||
clearCache: false,
|
||||
clearCache: clearCache, // 传递 clearCache 参数
|
||||
...filters, // 先展开筛选条件
|
||||
page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖)
|
||||
})).unwrap();
|
||||
@@ -175,108 +146,94 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
} finally {
|
||||
setLoadingPage(null);
|
||||
}
|
||||
}, [dispatch, pageSize, toast]);
|
||||
}, [dispatch, pageSize, toast, mode, filters]);
|
||||
|
||||
// 翻页处理(简化版 - 无预加载)
|
||||
// 翻页处理(第1页强制刷新 + 其他页缓存)
|
||||
const handlePageChange = useCallback(async (newPage) => {
|
||||
// 边界检查 1: 检查页码范围
|
||||
if (newPage < 1 || newPage > totalPages) {
|
||||
console.log(`%c⚠️ [翻页] 页码超出范围: ${newPage}`, 'color: #DC2626; font-weight: bold;');
|
||||
logger.warn('usePagination', '页码超出范围', { newPage, totalPages });
|
||||
return;
|
||||
}
|
||||
|
||||
// 边界检查 2: 检查是否重复点击
|
||||
if (newPage === currentPage) {
|
||||
console.log(`%c⚠️ [翻页] 重复点击当前页: ${newPage}`, 'color: #EAB308; font-weight: bold;');
|
||||
logger.debug('usePagination', '页码未改变', { newPage });
|
||||
return;
|
||||
}
|
||||
|
||||
// 边界检查 3: 防止竞态条件 - 如果正在加载其他页面,忽略新请求
|
||||
if (loadingPage !== null) {
|
||||
console.log(`%c⚠️ [翻页] 正在加载第${loadingPage}页,忽略新请求第${newPage}页`, 'color: #EAB308; font-weight: bold;');
|
||||
logger.warn('usePagination', '竞态条件:正在加载中', { loadingPage, newPage });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;');
|
||||
console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 总页数: ${totalPages}`, 'color: #3B82F6;');
|
||||
console.log(`%c 每页大小: ${pageSize}, 缓存总数: ${allCachedEvents.length}, 服务端总数: ${total}`, 'color: #3B82F6;');
|
||||
console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 模式: ${mode}`, 'color: #3B82F6;');
|
||||
|
||||
logger.debug('DynamicNewsCard', '开始翻页', {
|
||||
currentPage,
|
||||
newPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
total,
|
||||
allCachedEventsLength: allCachedEvents.length,
|
||||
cachedCount
|
||||
});
|
||||
// 【核心逻辑】第1页特殊处理:强制清空缓存并重新加载
|
||||
if (newPage === 1) {
|
||||
console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;');
|
||||
logger.info('usePagination', '第1页:强制刷新', { mode });
|
||||
|
||||
// 检查目标页缓存状态(统一处理,包括第一页)
|
||||
const { isTargetPageCached, targetPageInfo } = checkTargetPageCache(newPage);
|
||||
const success = await loadPage(newPage, true); // clearCache = true
|
||||
|
||||
console.log(`%c🟡 [缓存检查] 目标页${newPage}缓存状态`, 'color: #EAB308; font-weight: bold;');
|
||||
console.log(`%c 是否已缓存: ${isTargetPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isTargetPageCached ? '#16A34A' : '#DC2626'};`);
|
||||
console.log(`%c 索引范围: ${targetPageInfo.startIndex}-${targetPageInfo.endIndex}`, 'color: #EAB308;');
|
||||
console.log(`%c 实际数量: ${targetPageInfo.validCount}, 期望数量: ${targetPageInfo.expectedCount}`, 'color: #EAB308;');
|
||||
|
||||
if (isTargetPageCached) {
|
||||
// 目标页已缓存,直接切换
|
||||
console.log(`%c🟡 [缓存] 目标页已缓存,直接切换到第${newPage}页`, 'color: #16A34A; font-weight: bold;');
|
||||
logger.debug('DynamicNewsCard', '目标页已缓存,直接切换', { newPage });
|
||||
setCurrentPage(newPage);
|
||||
} else {
|
||||
// 目标页未缓存,显示 loading 并加载数据
|
||||
console.log(`%c🟡 [缓存] 目标页未缓存,需要加载第${newPage}页数据`, 'color: #DC2626; font-weight: bold;');
|
||||
logger.debug('DynamicNewsCard', '目标页未缓存,加载数据', { newPage });
|
||||
const success = await loadPage(newPage);
|
||||
|
||||
// 加载成功后切换页面
|
||||
if (success) {
|
||||
console.log(`%c🟢 [加载成功] 切换到第${newPage}页`, 'color: #16A34A; font-weight: bold;');
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 【其他页】检查缓存
|
||||
if (mode === DISPLAY_MODES.VERTICAL) {
|
||||
// 纵向模式:检查页码映射中是否有缓存
|
||||
const isPageCached = allCachedEventsByPage?.[newPage]?.length > 0;
|
||||
|
||||
console.log(`%c🟡 [缓存检查] 第${newPage}页缓存状态`, 'color: #EAB308; font-weight: bold;');
|
||||
console.log(`%c 是否已缓存: ${isPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isPageCached ? '#16A34A' : '#DC2626'};`);
|
||||
|
||||
if (isPageCached) {
|
||||
console.log(`%c✅ [缓存] 第${newPage}页已缓存,直接切换`, 'color: #16A34A; font-weight: bold;');
|
||||
setCurrentPage(newPage);
|
||||
} else {
|
||||
console.log(`%c❌ [加载失败] 未能切换到第${newPage}页`, 'color: #DC2626; font-weight: bold;');
|
||||
console.log(`%c❌ [缓存] 第${newPage}页未缓存,加载数据`, 'color: #DC2626; font-weight: bold;');
|
||||
const success = await loadPage(newPage, false); // clearCache = false
|
||||
|
||||
if (success) {
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
total,
|
||||
allCachedEvents.length,
|
||||
cachedCount,
|
||||
checkTargetPageCache,
|
||||
loadPage,
|
||||
dispatch,
|
||||
toast
|
||||
]);
|
||||
|
||||
// 更新累积列表(四排模式专用)
|
||||
useEffect(() => {
|
||||
if (isAccumulateMode) {
|
||||
// 计算已加载的所有事件(从第1页到当前页)
|
||||
const startIndex = 0;
|
||||
const endIndex = currentPage * pageSize;
|
||||
const accumulated = allCachedEvents.slice(startIndex, endIndex).filter(e => e !== null);
|
||||
|
||||
logger.debug('DynamicNewsCard', '更新累积事件列表', {
|
||||
currentPage,
|
||||
pageSize,
|
||||
startIndex,
|
||||
endIndex,
|
||||
accumulatedLength: accumulated.length
|
||||
});
|
||||
|
||||
setAccumulatedEvents(accumulated);
|
||||
} else {
|
||||
// 非累积模式时清空累积列表
|
||||
if (accumulatedEvents.length > 0) {
|
||||
setAccumulatedEvents([]);
|
||||
// 平铺模式:直接加载新页(追加模式,clearCache=false)
|
||||
console.log(`%c🟡 [平铺模式] 加载第${newPage}页`, 'color: #EAB308; font-weight: bold;');
|
||||
const success = await loadPage(newPage, false); // clearCache = false
|
||||
|
||||
if (success) {
|
||||
setCurrentPage(newPage);
|
||||
}
|
||||
}
|
||||
}, [isAccumulateMode, currentPage, pageSize, allCachedEvents]);
|
||||
}, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]);
|
||||
|
||||
|
||||
// 加载下一页(用于无限滚动)
|
||||
const loadNextPage = useCallback(async () => {
|
||||
// 修复:使用 hasMore 判断而不是 currentPage >= totalPages
|
||||
// 原因:去重后 cachedCount 可能小于 total,但 currentPage 已达到 totalPages
|
||||
// 使用 hasMore 判断(基于 currentPage < totalPages)
|
||||
if (!hasMore || loadingPage !== null) {
|
||||
logger.debug('DynamicNewsCard', '无法加载下一页', {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasMore,
|
||||
cachedCount,
|
||||
total,
|
||||
loadingPage,
|
||||
reason: !hasMore ? '已加载全部数据 (cachedCount >= total)' : '正在加载中'
|
||||
reason: !hasMore ? '已加载全部数据 (currentPage >= totalPages)' : '正在加载中'
|
||||
});
|
||||
return Promise.resolve(false); // 没有更多数据或正在加载
|
||||
}
|
||||
|
||||
const nextPage = currentPage + 1;
|
||||
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, cachedCount, total });
|
||||
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, totalPages });
|
||||
|
||||
try {
|
||||
await handlePageChange(nextPage);
|
||||
@@ -285,7 +242,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage });
|
||||
return false;
|
||||
}
|
||||
}, [currentPage, totalPages, hasMore, cachedCount, total, loadingPage, handlePageChange]);
|
||||
}, [currentPage, totalPages, hasMore, loadingPage, handlePageChange]);
|
||||
|
||||
// 加载上一页(用于双向无限滚动)
|
||||
const loadPrevPage = useCallback(async () => {
|
||||
@@ -328,13 +285,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
|
||||
totalPages,
|
||||
hasMore,
|
||||
currentPageEvents,
|
||||
displayEvents, // 新增:当前显示的事件列表(累积或分页)
|
||||
isAccumulateMode, // 新增:是否累积模式
|
||||
displayEvents, // 当前显示的事件列表
|
||||
|
||||
// 方法
|
||||
handlePageChange,
|
||||
handleModeToggle,
|
||||
loadNextPage, // 新增:加载下一页(用于无限滚动)
|
||||
loadPrevPage // 新增:加载上一页(用于双向无限滚动)
|
||||
loadNextPage, // 加载下一页(用于无限滚动)
|
||||
loadPrevPage // 加载上一页(用于双向无限滚动)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ const Community = () => {
|
||||
</Container>
|
||||
|
||||
{/* 事件弹窗 */}
|
||||
<EventModals
|
||||
{/* <EventModals
|
||||
eventModalState={{
|
||||
isOpen: !!selectedEvent,
|
||||
onClose: () => setSelectedEvent(null),
|
||||
@@ -199,7 +199,7 @@ const Community = () => {
|
||||
event: selectedEventForStock,
|
||||
onClose: () => setSelectedEventForStock(null)
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user