diff --git a/src/store/slices/communityDataSlice.js b/src/store/slices/communityDataSlice.js index 04461618..70054f1f 100644 --- a/src/store/slices/communityDataSlice.js +++ b/src/store/slices/communityDataSlice.js @@ -157,9 +157,10 @@ export const fetchHotEvents = createAsyncThunk( ); /** - * 获取动态新闻(客户端缓存 + 智能请求) + * 获取动态新闻(客户端缓存 + 虚拟滚动) * 用于 DynamicNewsCard 组件 * @param {Object} params - 请求参数 + * @param {string} params.mode - 显示模式('vertical' | 'four-row') * @param {number} params.page - 页码 * @param {number} params.per_page - 每页数量 * @param {boolean} params.clearCache - 是否清空缓存(默认 false) @@ -173,9 +174,10 @@ export const fetchHotEvents = createAsyncThunk( export const fetchDynamicNews = createAsyncThunk( 'communityData/fetchDynamicNews', async ({ + mode = 'vertical', page = 1, per_page = 5, - pageSize = 5, // 每页实际显示的数据量(用于计算索引) + pageSize = 5, // 🔍 添加 pageSize 参数(之前漏掉了) clearCache = false, prependMode = false, sort = 'new', @@ -214,11 +216,12 @@ export const fetchDynamicNews = createAsyncThunk( total: response.data.pagination?.total || 0 }); return { + mode, events: response.data.events, total: response.data.pagination?.total || 0, page, per_page, - pageSize, // 返回 pageSize 用于索引计算 + pageSize, // 🔍 添加 pageSize 到返回值 clearCache, prependMode }; @@ -226,13 +229,15 @@ export const fetchDynamicNews = createAsyncThunk( logger.warn('CommunityData', '动态新闻返回数据为空', response); return { + mode, events: [], total: 0, page, per_page, - pageSize, // 返回 pageSize 用于索引计算 + pageSize, // 🔍 添加 pageSize 到返回值 clearCache, - prependMode + prependMode, + isEmpty: true // 标记为空数据,用于边界条件处理 }; } catch (error) { logger.error('CommunityData', '获取动态新闻失败', error); @@ -294,36 +299,48 @@ const communityDataSlice = createSlice({ // 数据 popularKeywords: [], hotEvents: [], - dynamicNews: [], // 动态新闻完整缓存列表 - dynamicNewsTotal: 0, // 服务端总数量 - eventFollowStatus: {}, // 事件关注状态 { [eventId]: { isFollowing: boolean, followerCount: number } } + + // 纵向模式数据(独立存储) + verticalEvents: [], // 纵向模式完整缓存列表 + verticalTotal: 0, // 纵向模式服务端总数量 + verticalCachedCount: 0, // 纵向模式已缓存数量 + + // 平铺模式数据(独立存储) + fourRowEvents: [], // 平铺模式完整缓存列表 + fourRowTotal: 0, // 平铺模式服务端总数量 + fourRowCachedCount: 0, // 平铺模式已缓存数量 + + eventFollowStatus: {}, // 事件关注状态 { [eventId]: { isFollowing: boolean, followerCount: number } } // 加载状态 loading: { popularKeywords: false, hotEvents: false, - dynamicNews: false + verticalEvents: false, + fourRowEvents: false }, // 错误信息 error: { popularKeywords: null, hotEvents: null, - dynamicNews: null + verticalEvents: null, + fourRowEvents: null }, // 最后更新时间 lastUpdated: { popularKeywords: null, hotEvents: null, - dynamicNews: null + verticalEvents: null, + fourRowEvents: null } }, reducers: { /** * 清除所有缓存(Redux + localStorage) - * 注意:dynamicNews 不使用 localStorage 缓存 + * 注意:verticalEvents 和 fourRowEvents 不使用 localStorage 缓存 */ clearCache: (state) => { // 清除 localStorage @@ -332,17 +349,27 @@ const communityDataSlice = createSlice({ // 清除 Redux 状态 state.popularKeywords = []; state.hotEvents = []; - state.dynamicNews = []; // 动态新闻也清除 + + // 清除动态新闻数据(两个模式) + state.verticalEvents = []; + state.fourRowEvents = []; + state.verticalTotal = 0; + state.fourRowTotal = 0; + state.verticalCachedCount = 0; + state.fourRowCachedCount = 0; + + // 清除更新时间 state.lastUpdated.popularKeywords = null; state.lastUpdated.hotEvents = null; - state.lastUpdated.dynamicNews = null; + state.lastUpdated.verticalEvents = null; + state.lastUpdated.fourRowEvents = null; logger.info('CommunityData', '所有缓存已清除'); }, /** * 清除指定类型的缓存 - * @param {string} payload - 缓存类型 ('popularKeywords' | 'hotEvents' | 'dynamicNews') + * @param {string} payload - 缓存类型 ('popularKeywords' | 'hotEvents' | 'verticalEvents' | 'fourRowEvents') */ clearSpecificCache: (state, action) => { const type = action.payload; @@ -357,11 +384,20 @@ const communityDataSlice = createSlice({ state.hotEvents = []; state.lastUpdated.hotEvents = null; logger.info('CommunityData', '热点事件缓存已清除'); - } else if (type === 'dynamicNews') { - // dynamicNews 不使用 localStorage,只清除 Redux state - state.dynamicNews = []; - state.lastUpdated.dynamicNews = null; - logger.info('CommunityData', '动态新闻数据已清除'); + } else if (type === 'verticalEvents') { + // verticalEvents 不使用 localStorage,只清除 Redux state + state.verticalEvents = []; + state.verticalTotal = 0; + state.verticalCachedCount = 0; + state.lastUpdated.verticalEvents = null; + logger.info('CommunityData', '纵向模式事件数据已清除'); + } else if (type === 'fourRowEvents') { + // fourRowEvents 不使用 localStorage,只清除 Redux state + state.fourRowEvents = []; + state.fourRowTotal = 0; + state.fourRowCachedCount = 0; + state.lastUpdated.fourRowEvents = null; + logger.info('CommunityData', '平铺模式事件数据已清除'); } }, @@ -369,7 +405,7 @@ const communityDataSlice = createSlice({ * 预加载数据(用于应用启动时) * 注意:这不是异步 action,只是触发标记 */ - preloadData: (state) => { + preloadData: (_state) => { logger.info('CommunityData', '准备预加载数据'); // 实际的预加载逻辑在组件中调用 dispatch(fetchPopularKeywords()) 等 }, @@ -391,101 +427,90 @@ const communityDataSlice = createSlice({ createDataReducers(builder, fetchHotEvents, 'hotEvents'); // dynamicNews 需要特殊处理(缓存 + 追加模式) + // 根据 mode 更新不同的 state(verticalEvents 或 fourRowEvents) builder - .addCase(fetchDynamicNews.pending, (state) => { - state.loading.dynamicNews = true; - state.error.dynamicNews = null; + .addCase(fetchDynamicNews.pending, (state, action) => { + const mode = action.meta.arg.mode || 'vertical'; + const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents'; + state.loading[stateKey] = true; + state.error[stateKey] = null; }) .addCase(fetchDynamicNews.fulfilled, (state, action) => { - const { events, total, page, per_page, pageSize, clearCache, prependMode } = action.payload; + const { mode, events, total, page, clearCache, prependMode, isEmpty } = action.payload; + const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents'; + const totalKey = mode === 'four-row' ? 'fourRowTotal' : 'verticalTotal'; + const cachedCountKey = mode === 'four-row' ? 'fourRowCachedCount' : 'verticalCachedCount'; + + // 边界条件:空数据只记录日志,不更新 state(保留现有数据) + if (isEmpty || (events.length === 0 && !clearCache)) { + logger.info('CommunityData', `${mode} 模式返回空数据,跳过更新`); + state.loading[stateKey] = false; + state.error[stateKey] = '暂无更多数据'; // 设置提示信息供组件显示 toast + return; // 提前返回,不更新数据 + } + + // 🔍 调试:收到数据 + console.log('%c[Redux] fetchDynamicNews.fulfilled 收到数据', 'color: #10B981; font-weight: bold;', { + mode, + stateKey, + eventsCount: events.length, + total, + page, + clearCache, + prependMode, + 'state[stateKey] 之前': state[stateKey].length, + }); if (clearCache) { // 清空缓存模式:直接替换 - state.dynamicNews = events; - logger.debug('CommunityData', '清空缓存并加载新数据', { + state[stateKey] = events; + logger.debug('CommunityData', `清空缓存并加载新数据 (${mode})`, { count: events.length }); + + // 🔍 调试:清空缓存后的状态 + console.log('%c[Redux] clearCache 模式,直接替换数据', 'color: #10B981; font-weight: bold;', { + 'state[stateKey] 之后': state[stateKey].length + }); } else if (prependMode) { // 追加到头部模式(用于定时刷新):去重后插入头部 - const existingIds = new Set(state.dynamicNews.map(e => e.id)); + const existingIds = new Set(state[stateKey].map(e => e.id)); const newEvents = events.filter(e => !existingIds.has(e.id)); - state.dynamicNews = [...newEvents, ...state.dynamicNews]; - logger.debug('CommunityData', '追加新数据到头部', { + state[stateKey] = [...newEvents, ...state[stateKey]]; + logger.debug('CommunityData', `追加新数据到头部 (${mode})`, { newCount: newEvents.length, - totalCount: state.dynamicNews.length + totalCount: state[stateKey].length }); } else { - // 智能插入模式:根据页码计算正确的插入位置 - // 使用 pageSize(每页显示量)而不是 per_page(请求数量) - const startIndex = (page - 1) * (pageSize || per_page); + // 简单追加模式:去重后追加到末尾(虚拟滚动组件处理展示逻辑) + const existingIds = new Set(state[stateKey].map(e => e.id)); + const newEvents = events.filter(e => !existingIds.has(e.id)); + state[stateKey] = [...state[stateKey], ...newEvents]; - // 判断插入模式 - const isAppend = startIndex === state.dynamicNews.length; - const isReplace = startIndex < state.dynamicNews.length; - const isJump = startIndex > state.dynamicNews.length; - - // 只在 append 模式下去重(避免定时刷新重复) - // 替换和跳页模式直接使用原始数据(避免因去重导致数据丢失) - if (isAppend) { - // Append 模式:连续加载,需要去重 - const existingIds = new Set( - state.dynamicNews - .filter(e => e !== null) - .map(e => e.id) - ); - const newEvents = events.filter(e => !existingIds.has(e.id)); - state.dynamicNews = [...state.dynamicNews, ...newEvents]; - - logger.debug('CommunityData', '连续追加数据(去重)', { - page, - startIndex, - endIndex: startIndex + newEvents.length, - originalEventsCount: events.length, - newEventsCount: newEvents.length, - filteredCount: events.length - newEvents.length, - totalCount: state.dynamicNews.length - }); - } else if (isReplace) { - // 替换模式:直接覆盖,不去重 - const endIndex = startIndex + events.length; - const before = state.dynamicNews.slice(0, startIndex); - const after = state.dynamicNews.slice(endIndex); - state.dynamicNews = [...before, ...events, ...after]; - - logger.debug('CommunityData', '替换重叠数据(不去重)', { - page, - startIndex, - endIndex, - eventsCount: events.length, - beforeLength: before.length, - afterLength: after.length, - totalCount: state.dynamicNews.length - }); - } else { - // 跳页模式:填充间隔,不去重 - const gap = startIndex - state.dynamicNews.length; - const fillers = Array(gap).fill(null); - state.dynamicNews = [...state.dynamicNews, ...fillers, ...events]; - - logger.debug('CommunityData', '跳页加载,填充间隔(不去重)', { - page, - startIndex, - endIndex: startIndex + events.length, - gap, - eventsCount: events.length, - totalCount: state.dynamicNews.length - }); - } + logger.debug('CommunityData', `追加新数据(去重,${mode})`, { + page, + originalEventsCount: events.length, + newEventsCount: newEvents.length, + filteredCount: events.length - newEvents.length, + totalCount: state[stateKey].length + }); } - state.dynamicNewsTotal = total; - state.loading.dynamicNews = false; - state.lastUpdated.dynamicNews = new Date().toISOString(); + state[totalKey] = total; + state[cachedCountKey] = state[stateKey].length; // 简化:不再有 null 占位符 + + [`state.${stateKey}.length`]: state[stateKey].length + }); + + state.loading[stateKey] = false; + state.lastUpdated[stateKey] = new Date().toISOString(); }) .addCase(fetchDynamicNews.rejected, (state, action) => { - state.loading.dynamicNews = false; - state.error.dynamicNews = action.payload; - logger.error('CommunityData', 'dynamicNews 加载失败', new Error(action.payload)); + const mode = action.meta.arg.mode || 'vertical'; + const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents'; + state.loading[stateKey] = false; + state.error[stateKey] = action.payload; + logger.error('CommunityData', `${stateKey} 加载失败`, new Error(action.payload)); }) // toggleEventFollow .addCase(toggleEventFollow.fulfilled, (state, action) => { @@ -493,7 +518,7 @@ const communityDataSlice = createSlice({ state.eventFollowStatus[eventId] = { isFollowing, followerCount }; logger.debug('CommunityData', 'toggleEventFollow fulfilled', { eventId, isFollowing, followerCount }); }) - .addCase(toggleEventFollow.rejected, (state, action) => { + .addCase(toggleEventFollow.rejected, (_state, action) => { logger.error('CommunityData', 'toggleEventFollow rejected', action.payload); }); } @@ -506,12 +531,21 @@ export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus // 基础选择器(Selectors) export const selectPopularKeywords = (state) => state.communityData.popularKeywords; export const selectHotEvents = (state) => state.communityData.hotEvents; -export const selectDynamicNews = (state) => state.communityData.dynamicNews; export const selectEventFollowStatus = (state) => state.communityData.eventFollowStatus; export const selectLoading = (state) => state.communityData.loading; export const selectError = (state) => state.communityData.error; export const selectLastUpdated = (state) => state.communityData.lastUpdated; +// 纵向模式数据选择器 +export const selectVerticalEvents = (state) => state.communityData.verticalEvents; +export const selectVerticalTotal = (state) => state.communityData.verticalTotal; +export const selectVerticalCachedCount = (state) => state.communityData.verticalCachedCount; + +// 平铺模式数据选择器 +export const selectFourRowEvents = (state) => state.communityData.fourRowEvents; +export const selectFourRowTotal = (state) => state.communityData.fourRowTotal; +export const selectFourRowCachedCount = (state) => state.communityData.fourRowCachedCount; + // 组合选择器 export const selectPopularKeywordsWithLoading = (state) => ({ data: state.communityData.popularKeywords, @@ -527,13 +561,24 @@ export const selectHotEventsWithLoading = (state) => ({ lastUpdated: state.communityData.lastUpdated.hotEvents }); -export const selectDynamicNewsWithLoading = (state) => ({ - data: state.communityData.dynamicNews, // 完整缓存列表(可能包含 null 占位符) - loading: state.communityData.loading.dynamicNews, - error: state.communityData.error.dynamicNews, - total: state.communityData.dynamicNewsTotal, // 服务端总数量 - cachedCount: state.communityData.dynamicNews.filter(e => e !== null).length, // 已缓存有效数量(排除 null) - lastUpdated: state.communityData.lastUpdated.dynamicNews +// 纵向模式数据 + 加载状态选择器 +export const selectVerticalEventsWithLoading = (state) => ({ + data: state.communityData.verticalEvents, // 完整缓存列表(可能包含 null 占位符) + loading: state.communityData.loading.verticalEvents, + error: state.communityData.error.verticalEvents, + total: state.communityData.verticalTotal, // 服务端总数量 + cachedCount: state.communityData.verticalCachedCount, // 已缓存有效数量(排除 null) + lastUpdated: state.communityData.lastUpdated.verticalEvents +}); + +// 平铺模式数据 + 加载状态选择器 +export const selectFourRowEventsWithLoading = (state) => ({ + data: state.communityData.fourRowEvents, // 完整缓存列表(可能包含 null 占位符) + loading: state.communityData.loading.fourRowEvents, + error: state.communityData.error.fourRowEvents, + total: state.communityData.fourRowTotal, // 服务端总数量 + cachedCount: state.communityData.fourRowCachedCount, // 已缓存有效数量(排除 null) + lastUpdated: state.communityData.lastUpdated.fourRowEvents }); // 工具函数:检查数据是否需要刷新(超过指定时间)