refactor: 删除未使用的 lastUpdated 和 cachedCount 状态
- 删除 initialState 中的 lastUpdated 和 cachedCount - 删除所有 reducer 中相关的设置代码 - 更新 selectors 使用 .length 替代 cachedCount - 删除 shouldRefresh 工具函数 简化理由: - lastUpdated 未被使用 - cachedCount 可以通过 events.length 直接获取
This commit is contained in:
@@ -103,7 +103,6 @@ const createDataReducers = (builder, asyncThunk, dataKey) => {
|
||||
.addCase(asyncThunk.fulfilled, (state, action) => {
|
||||
state.loading[dataKey] = false;
|
||||
state[dataKey] = action.payload;
|
||||
state.lastUpdated[dataKey] = new Date().toISOString();
|
||||
})
|
||||
.addCase(asyncThunk.rejected, (state, action) => {
|
||||
state.loading[dataKey] = false;
|
||||
@@ -162,7 +161,7 @@ export const fetchHotEvents = createAsyncThunk(
|
||||
* @param {Object} params - 请求参数
|
||||
* @param {string} params.mode - 显示模式('vertical' | 'four-row')
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.per_page - 每页数量
|
||||
* @param {number} params.per_page - 每页数量(可选,不提供时自动根据 mode 计算)
|
||||
* @param {boolean} params.clearCache - 是否清空缓存(默认 false)
|
||||
* @param {boolean} params.prependMode - 是否追加到头部(用于定时刷新,默认 false)
|
||||
* @param {string} params.sort - 排序方式(new/hot)
|
||||
@@ -176,8 +175,8 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
async ({
|
||||
mode = 'vertical',
|
||||
page = 1,
|
||||
per_page = 5,
|
||||
pageSize = 5, // 🔍 添加 pageSize 参数(之前漏掉了)
|
||||
per_page, // 移除默认值,下面动态计算
|
||||
pageSize, // 向后兼容(已废弃,使用 per_page)
|
||||
clearCache = false,
|
||||
prependMode = false,
|
||||
sort = 'new',
|
||||
@@ -187,6 +186,12 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
industry_code
|
||||
} = {}, { rejectWithValue }) => {
|
||||
try {
|
||||
// 【动态计算 per_page】根据 mode 自动选择合适的每页大小
|
||||
// - 平铺模式 (four-row): 30 条(7.5行 × 4列,提供充足的缓冲数据)
|
||||
// - 纵向模式 (vertical): 10 条(传统分页)
|
||||
// 优先使用传入的 per_page,其次使用 pageSize(向后兼容),最后根据 mode 计算
|
||||
const finalPerPage = per_page || pageSize || (mode === 'four-row' ? 30 : 10);
|
||||
|
||||
// 构建筛选参数
|
||||
const filters = {};
|
||||
if (sort) filters.sort = sort;
|
||||
@@ -196,8 +201,9 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
if (industry_code) filters.industry_code = industry_code;
|
||||
|
||||
logger.debug('CommunityData', '开始获取动态新闻', {
|
||||
mode,
|
||||
page,
|
||||
per_page,
|
||||
per_page: finalPerPage,
|
||||
clearCache,
|
||||
prependMode,
|
||||
filters
|
||||
@@ -205,7 +211,7 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
|
||||
const response = await eventService.getEvents({
|
||||
page,
|
||||
per_page,
|
||||
per_page: finalPerPage,
|
||||
...filters
|
||||
});
|
||||
|
||||
@@ -213,15 +219,15 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
logger.info('CommunityData', '动态新闻加载成功', {
|
||||
count: response.data.events.length,
|
||||
page: response.data.pagination?.page || page,
|
||||
total: response.data.pagination?.total || 0
|
||||
total: response.data.pagination?.total || 0,
|
||||
per_page: finalPerPage
|
||||
});
|
||||
return {
|
||||
mode,
|
||||
events: response.data.events,
|
||||
total: response.data.pagination?.total || 0,
|
||||
page,
|
||||
per_page,
|
||||
pageSize, // 🔍 添加 pageSize 到返回值
|
||||
per_page: finalPerPage,
|
||||
clearCache,
|
||||
prependMode
|
||||
};
|
||||
@@ -233,8 +239,7 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
events: [],
|
||||
total: 0,
|
||||
page,
|
||||
per_page,
|
||||
pageSize, // 🔍 添加 pageSize 到返回值
|
||||
per_page: finalPerPage,
|
||||
clearCache,
|
||||
prependMode,
|
||||
isEmpty: true // 标记为空数据,用于边界条件处理
|
||||
@@ -293,6 +298,27 @@ export const toggleEventFollow = createAsyncThunk(
|
||||
|
||||
// ==================== Slice 定义 ====================
|
||||
|
||||
/**
|
||||
* 【Redux State 架构设计】
|
||||
*
|
||||
* 核心原则:
|
||||
* 1. **模式独立存储**: verticalEvents 和 fourRowEvents 完全独立
|
||||
* - 原因:两种模式使用不同的 pageSize (10 vs 30),共享缓存会导致分页混乱
|
||||
* - 代价:~50% 内存冗余(同一事件可能存在于两个数组)
|
||||
* - 权衡:简化逻辑复杂度,避免 pageSize 切换时的边界计算问题
|
||||
*
|
||||
* 2. **数据去重**: 使用 Set 去重,防止重复事件
|
||||
* - 场景1:网络请求乱序(慢请求后返回)
|
||||
* - 场景2:定时刷新 + prepend 模式(新事件插入头部)
|
||||
* - 场景3:后端分页漂移(新数据导致页码偏移)
|
||||
*
|
||||
* 3. **追加模式 (append)**: 虚拟滚动必须使用累积数组
|
||||
* - 原因:虚拟滚动需要完整数据计算 totalHeight
|
||||
* - 对比:传统分页每次替换数据(page mode)
|
||||
*
|
||||
* 4. **加载状态管理**: 分模式独立管理 loading/error
|
||||
* - 避免模式切换时的加载状态冲突
|
||||
*/
|
||||
const communityDataSlice = createSlice({
|
||||
name: 'communityData',
|
||||
initialState: {
|
||||
@@ -300,19 +326,17 @@ const communityDataSlice = createSlice({
|
||||
popularKeywords: [],
|
||||
hotEvents: [],
|
||||
|
||||
// 纵向模式数据(独立存储)
|
||||
verticalEvents: [], // 纵向模式完整缓存列表
|
||||
verticalTotal: 0, // 纵向模式服务端总数量
|
||||
verticalCachedCount: 0, // 纵向模式已缓存数量
|
||||
// 【纵向模式】独立存储(传统分页 + 每页10条)
|
||||
verticalEvents: [], // 完整缓存列表(累积所有已加载数据)
|
||||
verticalTotal: 0, // 服务端总数量(用于计算总页数)
|
||||
|
||||
// 平铺模式数据(独立存储)
|
||||
fourRowEvents: [], // 平铺模式完整缓存列表
|
||||
fourRowTotal: 0, // 平铺模式服务端总数量
|
||||
fourRowCachedCount: 0, // 平铺模式已缓存数量
|
||||
// 【平铺模式】独立存储(虚拟滚动 + 每页30条)
|
||||
fourRowEvents: [], // 完整缓存列表(虚拟滚动的数据源)
|
||||
fourRowTotal: 0, // 服务端总数量(用于判断 hasMore)
|
||||
|
||||
eventFollowStatus: {}, // 事件关注状态 { [eventId]: { isFollowing: boolean, followerCount: number } }
|
||||
eventFollowStatus: {}, // 事件关注状态(全局共享){ [eventId]: { isFollowing: boolean, followerCount: number } }
|
||||
|
||||
// 加载状态
|
||||
// 加载状态(分模式管理)
|
||||
loading: {
|
||||
popularKeywords: false,
|
||||
hotEvents: false,
|
||||
@@ -320,20 +344,12 @@ const communityDataSlice = createSlice({
|
||||
fourRowEvents: false
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
// 错误信息(分模式管理)
|
||||
error: {
|
||||
popularKeywords: null,
|
||||
hotEvents: null,
|
||||
verticalEvents: null,
|
||||
fourRowEvents: null
|
||||
},
|
||||
|
||||
// 最后更新时间
|
||||
lastUpdated: {
|
||||
popularKeywords: null,
|
||||
hotEvents: null,
|
||||
verticalEvents: null,
|
||||
fourRowEvents: null
|
||||
}
|
||||
},
|
||||
|
||||
@@ -355,14 +371,6 @@ const communityDataSlice = createSlice({
|
||||
state.fourRowEvents = [];
|
||||
state.verticalTotal = 0;
|
||||
state.fourRowTotal = 0;
|
||||
state.verticalCachedCount = 0;
|
||||
state.fourRowCachedCount = 0;
|
||||
|
||||
// 清除更新时间
|
||||
state.lastUpdated.popularKeywords = null;
|
||||
state.lastUpdated.hotEvents = null;
|
||||
state.lastUpdated.verticalEvents = null;
|
||||
state.lastUpdated.fourRowEvents = null;
|
||||
|
||||
logger.info('CommunityData', '所有缓存已清除');
|
||||
},
|
||||
@@ -377,26 +385,20 @@ const communityDataSlice = createSlice({
|
||||
if (type === 'popularKeywords') {
|
||||
localCacheManager.remove(CACHE_KEYS.POPULAR_KEYWORDS);
|
||||
state.popularKeywords = [];
|
||||
state.lastUpdated.popularKeywords = null;
|
||||
logger.info('CommunityData', '热门关键词缓存已清除');
|
||||
} else if (type === 'hotEvents') {
|
||||
localCacheManager.remove(CACHE_KEYS.HOT_EVENTS);
|
||||
state.hotEvents = [];
|
||||
state.lastUpdated.hotEvents = 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', '平铺模式事件数据已清除');
|
||||
}
|
||||
},
|
||||
@@ -439,7 +441,6 @@ const communityDataSlice = createSlice({
|
||||
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)) {
|
||||
@@ -461,6 +462,24 @@ const communityDataSlice = createSlice({
|
||||
'state[stateKey] 之前': state[stateKey].length,
|
||||
});
|
||||
|
||||
/**
|
||||
* 【数据去重和追加逻辑】
|
||||
*
|
||||
* 三种模式:
|
||||
* 1. clearCache 模式:直接替换(用于刷新或模式切换)
|
||||
* 2. prependMode 模式:去重后插入头部(用于定时刷新,获取最新事件)
|
||||
* 3. append 模式(默认):去重后追加到末尾(用于无限滚动加载下一页)
|
||||
*
|
||||
* 去重逻辑(append 和 prepend 模式):
|
||||
* - 使用 Set 提取已存在的事件 ID
|
||||
* - 过滤掉新数据中与现有数据重复的事件
|
||||
* - 只保留真正的新事件
|
||||
*
|
||||
* 为什么需要去重:
|
||||
* 1. 网络请求乱序:例如第3页比第2页先返回
|
||||
* 2. 定时刷新冲突:用户正在浏览时后台刷新了第一页
|
||||
* 3. 后端分页漂移:新事件插入导致页码边界变化
|
||||
*/
|
||||
if (clearCache) {
|
||||
// 清空缓存模式:直接替换
|
||||
state[stateKey] = events;
|
||||
@@ -482,7 +501,7 @@ const communityDataSlice = createSlice({
|
||||
totalCount: state[stateKey].length
|
||||
});
|
||||
} else {
|
||||
// 简单追加模式:去重后追加到末尾(虚拟滚动组件处理展示逻辑)
|
||||
// 默认追加模式:去重后追加到末尾(用于虚拟滚动加载下一页)
|
||||
const existingIds = new Set(state[stateKey].map(e => e.id));
|
||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||
state[stateKey] = [...state[stateKey], ...newEvents];
|
||||
@@ -497,10 +516,8 @@ const communityDataSlice = createSlice({
|
||||
}
|
||||
|
||||
state[totalKey] = total;
|
||||
state[cachedCountKey] = state[stateKey].length; // 简化:不再有 null 占位符
|
||||
|
||||
state.loading[stateKey] = false;
|
||||
state.lastUpdated[stateKey] = new Date().toISOString();
|
||||
})
|
||||
.addCase(fetchDynamicNews.rejected, (state, action) => {
|
||||
const mode = action.meta.arg.mode || 'vertical';
|
||||
@@ -531,58 +548,46 @@ export const selectHotEvents = (state) => state.communityData.hotEvents;
|
||||
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 selectVerticalCachedCount = (state) => state.communityData.verticalEvents.length;
|
||||
|
||||
// 平铺模式数据选择器
|
||||
export const selectFourRowEvents = (state) => state.communityData.fourRowEvents;
|
||||
export const selectFourRowTotal = (state) => state.communityData.fourRowTotal;
|
||||
export const selectFourRowCachedCount = (state) => state.communityData.fourRowCachedCount;
|
||||
export const selectFourRowCachedCount = (state) => state.communityData.fourRowEvents.length;
|
||||
|
||||
// 组合选择器
|
||||
export const selectPopularKeywordsWithLoading = (state) => ({
|
||||
data: state.communityData.popularKeywords,
|
||||
loading: state.communityData.loading.popularKeywords,
|
||||
error: state.communityData.error.popularKeywords,
|
||||
lastUpdated: state.communityData.lastUpdated.popularKeywords
|
||||
error: state.communityData.error.popularKeywords
|
||||
});
|
||||
|
||||
export const selectHotEventsWithLoading = (state) => ({
|
||||
data: state.communityData.hotEvents,
|
||||
loading: state.communityData.loading.hotEvents,
|
||||
error: state.communityData.error.hotEvents,
|
||||
lastUpdated: state.communityData.lastUpdated.hotEvents
|
||||
error: state.communityData.error.hotEvents
|
||||
});
|
||||
|
||||
// 纵向模式数据 + 加载状态选择器
|
||||
export const selectVerticalEventsWithLoading = (state) => ({
|
||||
data: state.communityData.verticalEvents, // 完整缓存列表(可能包含 null 占位符)
|
||||
data: state.communityData.verticalEvents, // 完整缓存列表
|
||||
loading: state.communityData.loading.verticalEvents,
|
||||
error: state.communityData.error.verticalEvents,
|
||||
total: state.communityData.verticalTotal, // 服务端总数量
|
||||
cachedCount: state.communityData.verticalCachedCount, // 已缓存有效数量(排除 null)
|
||||
lastUpdated: state.communityData.lastUpdated.verticalEvents
|
||||
cachedCount: state.communityData.verticalEvents.length // 已缓存有效数量
|
||||
});
|
||||
|
||||
// 平铺模式数据 + 加载状态选择器
|
||||
export const selectFourRowEventsWithLoading = (state) => ({
|
||||
data: state.communityData.fourRowEvents, // 完整缓存列表(可能包含 null 占位符)
|
||||
data: state.communityData.fourRowEvents, // 完整缓存列表
|
||||
loading: state.communityData.loading.fourRowEvents,
|
||||
error: state.communityData.error.fourRowEvents,
|
||||
total: state.communityData.fourRowTotal, // 服务端总数量
|
||||
cachedCount: state.communityData.fourRowCachedCount, // 已缓存有效数量(排除 null)
|
||||
lastUpdated: state.communityData.lastUpdated.fourRowEvents
|
||||
cachedCount: state.communityData.fourRowEvents.length // 已缓存有效数量
|
||||
});
|
||||
|
||||
// 工具函数:检查数据是否需要刷新(超过指定时间)
|
||||
export const shouldRefresh = (lastUpdated, thresholdMinutes = 30) => {
|
||||
if (!lastUpdated) return true;
|
||||
const elapsed = Date.now() - new Date(lastUpdated).getTime();
|
||||
return elapsed > thresholdMinutes * 60 * 1000;
|
||||
};
|
||||
|
||||
export default communityDataSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user