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) => {
|
.addCase(asyncThunk.fulfilled, (state, action) => {
|
||||||
state.loading[dataKey] = false;
|
state.loading[dataKey] = false;
|
||||||
state[dataKey] = action.payload;
|
state[dataKey] = action.payload;
|
||||||
state.lastUpdated[dataKey] = new Date().toISOString();
|
|
||||||
})
|
})
|
||||||
.addCase(asyncThunk.rejected, (state, action) => {
|
.addCase(asyncThunk.rejected, (state, action) => {
|
||||||
state.loading[dataKey] = false;
|
state.loading[dataKey] = false;
|
||||||
@@ -162,7 +161,7 @@ export const fetchHotEvents = createAsyncThunk(
|
|||||||
* @param {Object} params - 请求参数
|
* @param {Object} params - 请求参数
|
||||||
* @param {string} params.mode - 显示模式('vertical' | 'four-row')
|
* @param {string} params.mode - 显示模式('vertical' | 'four-row')
|
||||||
* @param {number} params.page - 页码
|
* @param {number} params.page - 页码
|
||||||
* @param {number} params.per_page - 每页数量
|
* @param {number} params.per_page - 每页数量(可选,不提供时自动根据 mode 计算)
|
||||||
* @param {boolean} params.clearCache - 是否清空缓存(默认 false)
|
* @param {boolean} params.clearCache - 是否清空缓存(默认 false)
|
||||||
* @param {boolean} params.prependMode - 是否追加到头部(用于定时刷新,默认 false)
|
* @param {boolean} params.prependMode - 是否追加到头部(用于定时刷新,默认 false)
|
||||||
* @param {string} params.sort - 排序方式(new/hot)
|
* @param {string} params.sort - 排序方式(new/hot)
|
||||||
@@ -176,8 +175,8 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
async ({
|
async ({
|
||||||
mode = 'vertical',
|
mode = 'vertical',
|
||||||
page = 1,
|
page = 1,
|
||||||
per_page = 5,
|
per_page, // 移除默认值,下面动态计算
|
||||||
pageSize = 5, // 🔍 添加 pageSize 参数(之前漏掉了)
|
pageSize, // 向后兼容(已废弃,使用 per_page)
|
||||||
clearCache = false,
|
clearCache = false,
|
||||||
prependMode = false,
|
prependMode = false,
|
||||||
sort = 'new',
|
sort = 'new',
|
||||||
@@ -187,6 +186,12 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
industry_code
|
industry_code
|
||||||
} = {}, { rejectWithValue }) => {
|
} = {}, { rejectWithValue }) => {
|
||||||
try {
|
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 = {};
|
const filters = {};
|
||||||
if (sort) filters.sort = sort;
|
if (sort) filters.sort = sort;
|
||||||
@@ -196,8 +201,9 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
if (industry_code) filters.industry_code = industry_code;
|
if (industry_code) filters.industry_code = industry_code;
|
||||||
|
|
||||||
logger.debug('CommunityData', '开始获取动态新闻', {
|
logger.debug('CommunityData', '开始获取动态新闻', {
|
||||||
|
mode,
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page: finalPerPage,
|
||||||
clearCache,
|
clearCache,
|
||||||
prependMode,
|
prependMode,
|
||||||
filters
|
filters
|
||||||
@@ -205,7 +211,7 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
|
|
||||||
const response = await eventService.getEvents({
|
const response = await eventService.getEvents({
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page: finalPerPage,
|
||||||
...filters
|
...filters
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,15 +219,15 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
logger.info('CommunityData', '动态新闻加载成功', {
|
logger.info('CommunityData', '动态新闻加载成功', {
|
||||||
count: response.data.events.length,
|
count: response.data.events.length,
|
||||||
page: response.data.pagination?.page || page,
|
page: response.data.pagination?.page || page,
|
||||||
total: response.data.pagination?.total || 0
|
total: response.data.pagination?.total || 0,
|
||||||
|
per_page: finalPerPage
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
events: response.data.events,
|
events: response.data.events,
|
||||||
total: response.data.pagination?.total || 0,
|
total: response.data.pagination?.total || 0,
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page: finalPerPage,
|
||||||
pageSize, // 🔍 添加 pageSize 到返回值
|
|
||||||
clearCache,
|
clearCache,
|
||||||
prependMode
|
prependMode
|
||||||
};
|
};
|
||||||
@@ -233,8 +239,7 @@ export const fetchDynamicNews = createAsyncThunk(
|
|||||||
events: [],
|
events: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page: finalPerPage,
|
||||||
pageSize, // 🔍 添加 pageSize 到返回值
|
|
||||||
clearCache,
|
clearCache,
|
||||||
prependMode,
|
prependMode,
|
||||||
isEmpty: true // 标记为空数据,用于边界条件处理
|
isEmpty: true // 标记为空数据,用于边界条件处理
|
||||||
@@ -293,6 +298,27 @@ export const toggleEventFollow = createAsyncThunk(
|
|||||||
|
|
||||||
// ==================== Slice 定义 ====================
|
// ==================== 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({
|
const communityDataSlice = createSlice({
|
||||||
name: 'communityData',
|
name: 'communityData',
|
||||||
initialState: {
|
initialState: {
|
||||||
@@ -300,19 +326,17 @@ const communityDataSlice = createSlice({
|
|||||||
popularKeywords: [],
|
popularKeywords: [],
|
||||||
hotEvents: [],
|
hotEvents: [],
|
||||||
|
|
||||||
// 纵向模式数据(独立存储)
|
// 【纵向模式】独立存储(传统分页 + 每页10条)
|
||||||
verticalEvents: [], // 纵向模式完整缓存列表
|
verticalEvents: [], // 完整缓存列表(累积所有已加载数据)
|
||||||
verticalTotal: 0, // 纵向模式服务端总数量
|
verticalTotal: 0, // 服务端总数量(用于计算总页数)
|
||||||
verticalCachedCount: 0, // 纵向模式已缓存数量
|
|
||||||
|
|
||||||
// 平铺模式数据(独立存储)
|
// 【平铺模式】独立存储(虚拟滚动 + 每页30条)
|
||||||
fourRowEvents: [], // 平铺模式完整缓存列表
|
fourRowEvents: [], // 完整缓存列表(虚拟滚动的数据源)
|
||||||
fourRowTotal: 0, // 平铺模式服务端总数量
|
fourRowTotal: 0, // 服务端总数量(用于判断 hasMore)
|
||||||
fourRowCachedCount: 0, // 平铺模式已缓存数量
|
|
||||||
|
|
||||||
eventFollowStatus: {}, // 事件关注状态 { [eventId]: { isFollowing: boolean, followerCount: number } }
|
eventFollowStatus: {}, // 事件关注状态(全局共享){ [eventId]: { isFollowing: boolean, followerCount: number } }
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态(分模式管理)
|
||||||
loading: {
|
loading: {
|
||||||
popularKeywords: false,
|
popularKeywords: false,
|
||||||
hotEvents: false,
|
hotEvents: false,
|
||||||
@@ -320,20 +344,12 @@ const communityDataSlice = createSlice({
|
|||||||
fourRowEvents: false
|
fourRowEvents: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// 错误信息
|
// 错误信息(分模式管理)
|
||||||
error: {
|
error: {
|
||||||
popularKeywords: null,
|
popularKeywords: null,
|
||||||
hotEvents: null,
|
hotEvents: null,
|
||||||
verticalEvents: null,
|
verticalEvents: null,
|
||||||
fourRowEvents: null
|
fourRowEvents: null
|
||||||
},
|
|
||||||
|
|
||||||
// 最后更新时间
|
|
||||||
lastUpdated: {
|
|
||||||
popularKeywords: null,
|
|
||||||
hotEvents: null,
|
|
||||||
verticalEvents: null,
|
|
||||||
fourRowEvents: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -355,14 +371,6 @@ const communityDataSlice = createSlice({
|
|||||||
state.fourRowEvents = [];
|
state.fourRowEvents = [];
|
||||||
state.verticalTotal = 0;
|
state.verticalTotal = 0;
|
||||||
state.fourRowTotal = 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', '所有缓存已清除');
|
logger.info('CommunityData', '所有缓存已清除');
|
||||||
},
|
},
|
||||||
@@ -377,26 +385,20 @@ const communityDataSlice = createSlice({
|
|||||||
if (type === 'popularKeywords') {
|
if (type === 'popularKeywords') {
|
||||||
localCacheManager.remove(CACHE_KEYS.POPULAR_KEYWORDS);
|
localCacheManager.remove(CACHE_KEYS.POPULAR_KEYWORDS);
|
||||||
state.popularKeywords = [];
|
state.popularKeywords = [];
|
||||||
state.lastUpdated.popularKeywords = null;
|
|
||||||
logger.info('CommunityData', '热门关键词缓存已清除');
|
logger.info('CommunityData', '热门关键词缓存已清除');
|
||||||
} else if (type === 'hotEvents') {
|
} else if (type === 'hotEvents') {
|
||||||
localCacheManager.remove(CACHE_KEYS.HOT_EVENTS);
|
localCacheManager.remove(CACHE_KEYS.HOT_EVENTS);
|
||||||
state.hotEvents = [];
|
state.hotEvents = [];
|
||||||
state.lastUpdated.hotEvents = null;
|
|
||||||
logger.info('CommunityData', '热点事件缓存已清除');
|
logger.info('CommunityData', '热点事件缓存已清除');
|
||||||
} else if (type === 'verticalEvents') {
|
} else if (type === 'verticalEvents') {
|
||||||
// verticalEvents 不使用 localStorage,只清除 Redux state
|
// verticalEvents 不使用 localStorage,只清除 Redux state
|
||||||
state.verticalEvents = [];
|
state.verticalEvents = [];
|
||||||
state.verticalTotal = 0;
|
state.verticalTotal = 0;
|
||||||
state.verticalCachedCount = 0;
|
|
||||||
state.lastUpdated.verticalEvents = null;
|
|
||||||
logger.info('CommunityData', '纵向模式事件数据已清除');
|
logger.info('CommunityData', '纵向模式事件数据已清除');
|
||||||
} else if (type === 'fourRowEvents') {
|
} else if (type === 'fourRowEvents') {
|
||||||
// fourRowEvents 不使用 localStorage,只清除 Redux state
|
// fourRowEvents 不使用 localStorage,只清除 Redux state
|
||||||
state.fourRowEvents = [];
|
state.fourRowEvents = [];
|
||||||
state.fourRowTotal = 0;
|
state.fourRowTotal = 0;
|
||||||
state.fourRowCachedCount = 0;
|
|
||||||
state.lastUpdated.fourRowEvents = null;
|
|
||||||
logger.info('CommunityData', '平铺模式事件数据已清除');
|
logger.info('CommunityData', '平铺模式事件数据已清除');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -439,7 +441,6 @@ const communityDataSlice = createSlice({
|
|||||||
const { mode, events, total, page, clearCache, prependMode, isEmpty } = action.payload;
|
const { mode, events, total, page, clearCache, prependMode, isEmpty } = action.payload;
|
||||||
const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents';
|
const stateKey = mode === 'four-row' ? 'fourRowEvents' : 'verticalEvents';
|
||||||
const totalKey = mode === 'four-row' ? 'fourRowTotal' : 'verticalTotal';
|
const totalKey = mode === 'four-row' ? 'fourRowTotal' : 'verticalTotal';
|
||||||
const cachedCountKey = mode === 'four-row' ? 'fourRowCachedCount' : 'verticalCachedCount';
|
|
||||||
|
|
||||||
// 边界条件:空数据只记录日志,不更新 state(保留现有数据)
|
// 边界条件:空数据只记录日志,不更新 state(保留现有数据)
|
||||||
if (isEmpty || (events.length === 0 && !clearCache)) {
|
if (isEmpty || (events.length === 0 && !clearCache)) {
|
||||||
@@ -461,6 +462,24 @@ const communityDataSlice = createSlice({
|
|||||||
'state[stateKey] 之前': state[stateKey].length,
|
'state[stateKey] 之前': state[stateKey].length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【数据去重和追加逻辑】
|
||||||
|
*
|
||||||
|
* 三种模式:
|
||||||
|
* 1. clearCache 模式:直接替换(用于刷新或模式切换)
|
||||||
|
* 2. prependMode 模式:去重后插入头部(用于定时刷新,获取最新事件)
|
||||||
|
* 3. append 模式(默认):去重后追加到末尾(用于无限滚动加载下一页)
|
||||||
|
*
|
||||||
|
* 去重逻辑(append 和 prepend 模式):
|
||||||
|
* - 使用 Set 提取已存在的事件 ID
|
||||||
|
* - 过滤掉新数据中与现有数据重复的事件
|
||||||
|
* - 只保留真正的新事件
|
||||||
|
*
|
||||||
|
* 为什么需要去重:
|
||||||
|
* 1. 网络请求乱序:例如第3页比第2页先返回
|
||||||
|
* 2. 定时刷新冲突:用户正在浏览时后台刷新了第一页
|
||||||
|
* 3. 后端分页漂移:新事件插入导致页码边界变化
|
||||||
|
*/
|
||||||
if (clearCache) {
|
if (clearCache) {
|
||||||
// 清空缓存模式:直接替换
|
// 清空缓存模式:直接替换
|
||||||
state[stateKey] = events;
|
state[stateKey] = events;
|
||||||
@@ -482,7 +501,7 @@ const communityDataSlice = createSlice({
|
|||||||
totalCount: state[stateKey].length
|
totalCount: state[stateKey].length
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 简单追加模式:去重后追加到末尾(虚拟滚动组件处理展示逻辑)
|
// 默认追加模式:去重后追加到末尾(用于虚拟滚动加载下一页)
|
||||||
const existingIds = new Set(state[stateKey].map(e => e.id));
|
const existingIds = new Set(state[stateKey].map(e => e.id));
|
||||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||||
state[stateKey] = [...state[stateKey], ...newEvents];
|
state[stateKey] = [...state[stateKey], ...newEvents];
|
||||||
@@ -497,10 +516,8 @@ const communityDataSlice = createSlice({
|
|||||||
}
|
}
|
||||||
|
|
||||||
state[totalKey] = total;
|
state[totalKey] = total;
|
||||||
state[cachedCountKey] = state[stateKey].length; // 简化:不再有 null 占位符
|
|
||||||
|
|
||||||
state.loading[stateKey] = false;
|
state.loading[stateKey] = false;
|
||||||
state.lastUpdated[stateKey] = new Date().toISOString();
|
|
||||||
})
|
})
|
||||||
.addCase(fetchDynamicNews.rejected, (state, action) => {
|
.addCase(fetchDynamicNews.rejected, (state, action) => {
|
||||||
const mode = action.meta.arg.mode || 'vertical';
|
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 selectEventFollowStatus = (state) => state.communityData.eventFollowStatus;
|
||||||
export const selectLoading = (state) => state.communityData.loading;
|
export const selectLoading = (state) => state.communityData.loading;
|
||||||
export const selectError = (state) => state.communityData.error;
|
export const selectError = (state) => state.communityData.error;
|
||||||
export const selectLastUpdated = (state) => state.communityData.lastUpdated;
|
|
||||||
|
|
||||||
// 纵向模式数据选择器
|
// 纵向模式数据选择器
|
||||||
export const selectVerticalEvents = (state) => state.communityData.verticalEvents;
|
export const selectVerticalEvents = (state) => state.communityData.verticalEvents;
|
||||||
export const selectVerticalTotal = (state) => state.communityData.verticalTotal;
|
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 selectFourRowEvents = (state) => state.communityData.fourRowEvents;
|
||||||
export const selectFourRowTotal = (state) => state.communityData.fourRowTotal;
|
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) => ({
|
export const selectPopularKeywordsWithLoading = (state) => ({
|
||||||
data: state.communityData.popularKeywords,
|
data: state.communityData.popularKeywords,
|
||||||
loading: state.communityData.loading.popularKeywords,
|
loading: state.communityData.loading.popularKeywords,
|
||||||
error: state.communityData.error.popularKeywords,
|
error: state.communityData.error.popularKeywords
|
||||||
lastUpdated: state.communityData.lastUpdated.popularKeywords
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectHotEventsWithLoading = (state) => ({
|
export const selectHotEventsWithLoading = (state) => ({
|
||||||
data: state.communityData.hotEvents,
|
data: state.communityData.hotEvents,
|
||||||
loading: state.communityData.loading.hotEvents,
|
loading: state.communityData.loading.hotEvents,
|
||||||
error: state.communityData.error.hotEvents,
|
error: state.communityData.error.hotEvents
|
||||||
lastUpdated: state.communityData.lastUpdated.hotEvents
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 纵向模式数据 + 加载状态选择器
|
// 纵向模式数据 + 加载状态选择器
|
||||||
export const selectVerticalEventsWithLoading = (state) => ({
|
export const selectVerticalEventsWithLoading = (state) => ({
|
||||||
data: state.communityData.verticalEvents, // 完整缓存列表(可能包含 null 占位符)
|
data: state.communityData.verticalEvents, // 完整缓存列表
|
||||||
loading: state.communityData.loading.verticalEvents,
|
loading: state.communityData.loading.verticalEvents,
|
||||||
error: state.communityData.error.verticalEvents,
|
error: state.communityData.error.verticalEvents,
|
||||||
total: state.communityData.verticalTotal, // 服务端总数量
|
total: state.communityData.verticalTotal, // 服务端总数量
|
||||||
cachedCount: state.communityData.verticalCachedCount, // 已缓存有效数量(排除 null)
|
cachedCount: state.communityData.verticalEvents.length // 已缓存有效数量
|
||||||
lastUpdated: state.communityData.lastUpdated.verticalEvents
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 平铺模式数据 + 加载状态选择器
|
// 平铺模式数据 + 加载状态选择器
|
||||||
export const selectFourRowEventsWithLoading = (state) => ({
|
export const selectFourRowEventsWithLoading = (state) => ({
|
||||||
data: state.communityData.fourRowEvents, // 完整缓存列表(可能包含 null 占位符)
|
data: state.communityData.fourRowEvents, // 完整缓存列表
|
||||||
loading: state.communityData.loading.fourRowEvents,
|
loading: state.communityData.loading.fourRowEvents,
|
||||||
error: state.communityData.error.fourRowEvents,
|
error: state.communityData.error.fourRowEvents,
|
||||||
total: state.communityData.fourRowTotal, // 服务端总数量
|
total: state.communityData.fourRowTotal, // 服务端总数量
|
||||||
cachedCount: state.communityData.fourRowCachedCount, // 已缓存有效数量(排除 null)
|
cachedCount: state.communityData.fourRowEvents.length // 已缓存有效数量
|
||||||
lastUpdated: state.communityData.lastUpdated.fourRowEvents
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 工具函数:检查数据是否需要刷新(超过指定时间)
|
|
||||||
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;
|
export default communityDataSlice.reducer;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const VirtualizedFourRowGrid = ({
|
|||||||
getTimelineBoxStyle,
|
getTimelineBoxStyle,
|
||||||
borderColor,
|
borderColor,
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
loadPrevPage, // 新增:加载上一页
|
onRefreshFirstPage, // 修改:顶部刷新回调(替代 loadPrevPage)
|
||||||
hasMore,
|
hasMore,
|
||||||
loading,
|
loading,
|
||||||
error, // 新增:错误状态
|
error, // 新增:错误状态
|
||||||
@@ -43,7 +43,7 @@ const VirtualizedFourRowGrid = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const parentRef = useRef(null);
|
const parentRef = useRef(null);
|
||||||
const isLoadingMore = useRef(false); // 防止重复加载
|
const isLoadingMore = useRef(false); // 防止重复加载
|
||||||
const previousScrollHeight = useRef(0); // 记录加载前的滚动高度(用于位置保持)
|
const lastRefreshTime = useRef(0); // 记录上次刷新时间(用于30秒防抖)
|
||||||
|
|
||||||
// 滚动条颜色(主题适配)
|
// 滚动条颜色(主题适配)
|
||||||
const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
|
const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
|
||||||
@@ -67,7 +67,30 @@ const VirtualizedFourRowGrid = ({
|
|||||||
overscan: 2, // 预加载2行(上下各1行)
|
overscan: 2, // 预加载2行(上下各1行)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 双向无限滚动逻辑 - 监听滚动事件,到达底部加载下一页,到达顶部加载上一页
|
/**
|
||||||
|
* 【核心逻辑1】无限滚动 + 顶部刷新 - 监听滚动事件,根据滚动位置自动加载数据或刷新
|
||||||
|
*
|
||||||
|
* 工作原理:
|
||||||
|
* 1. 向下滚动到 60% 位置时,触发 loadNextPage()
|
||||||
|
* - 调用 usePagination.loadNextPage()
|
||||||
|
* - 内部执行 handlePageChange(currentPage + 1)
|
||||||
|
* - dispatch(fetchDynamicNews({ page: nextPage }))
|
||||||
|
* - 后端返回下一页数据(30条)
|
||||||
|
* - Redux 去重后追加到 fourRowEvents 数组
|
||||||
|
* - events prop 更新,虚拟滚动自动渲染新内容
|
||||||
|
*
|
||||||
|
* 2. 向上滚动到顶部 10% 以内时,触发 onRefreshFirstPage()
|
||||||
|
* - 清空缓存 + 重新加载第一页(获取最新数据)
|
||||||
|
* - 30秒防抖:避免频繁刷新
|
||||||
|
* - 与5分钟定时刷新协同工作
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* - 60% 触发点:提前加载,避免滚动到底部时才出现加载状态
|
||||||
|
* - 防抖机制:isLoadingMore.current 防止重复触发
|
||||||
|
* - 两层缓存:
|
||||||
|
* - Redux 缓存(HTTP层):fourRowEvents 数组存储已加载数据,避免重复请求
|
||||||
|
* - 虚拟滚动缓存(渲染层):@tanstack/react-virtual 只渲染可见行,复用 DOM 节点
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollElement = parentRef.current;
|
const scrollElement = parentRef.current;
|
||||||
if (!scrollElement) return;
|
if (!scrollElement) return;
|
||||||
@@ -81,30 +104,56 @@ const VirtualizedFourRowGrid = ({
|
|||||||
|
|
||||||
// 向下滚动:滚动到 60% 时开始加载下一页
|
// 向下滚动:滚动到 60% 时开始加载下一页
|
||||||
if (loadNextPage && hasMore && scrollPercentage > 0.6) {
|
if (loadNextPage && hasMore && scrollPercentage > 0.6) {
|
||||||
console.log('%c📜 [双向滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
|
console.log('%c📜 [无限滚动] 到达底部,加载下一页', 'color: #8B5CF6; font-weight: bold;');
|
||||||
isLoadingMore.current = true;
|
isLoadingMore.current = true;
|
||||||
await loadNextPage();
|
await loadNextPage();
|
||||||
isLoadingMore.current = false;
|
isLoadingMore.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向上滚动:滚动到顶部 10% 以内时加载上一页
|
// 向上滚动到顶部:触发刷新(30秒防抖)
|
||||||
if (loadPrevPage && scrollTop < clientHeight * 0.1) {
|
if (onRefreshFirstPage && scrollTop < clientHeight * 0.1) {
|
||||||
console.log('%c📜 [双向滚动] 到达顶部,加载上一页', 'color: #10B981; font-weight: bold;');
|
const now = Date.now();
|
||||||
isLoadingMore.current = true;
|
const timeSinceLastRefresh = now - lastRefreshTime.current;
|
||||||
|
|
||||||
// 记录加载前的滚动高度(用于位置保持)
|
// 30秒防抖:避免频繁刷新
|
||||||
previousScrollHeight.current = scrollHeight;
|
if (timeSinceLastRefresh >= 30000) {
|
||||||
|
console.log('%c🔄 [顶部刷新] 滚动到顶部,清空缓存并重新加载第一页', 'color: #10B981; font-weight: bold;', {
|
||||||
|
timeSinceLastRefresh: `${(timeSinceLastRefresh / 1000).toFixed(1)}秒`
|
||||||
|
});
|
||||||
|
isLoadingMore.current = true;
|
||||||
|
lastRefreshTime.current = now;
|
||||||
|
|
||||||
await loadPrevPage();
|
await onRefreshFirstPage();
|
||||||
isLoadingMore.current = false;
|
isLoadingMore.current = false;
|
||||||
|
} else {
|
||||||
|
const remainingTime = Math.ceil((30000 - timeSinceLastRefresh) / 1000);
|
||||||
|
console.log('%c🔄 [顶部刷新] 防抖中,请等待', 'color: #EAB308; font-weight: bold;', {
|
||||||
|
remainingTime: `${remainingTime}秒`
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollElement.addEventListener('scroll', handleScroll);
|
scrollElement.addEventListener('scroll', handleScroll);
|
||||||
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
||||||
}, [loadNextPage, loadPrevPage, hasMore, loading]);
|
}, [loadNextPage, onRefreshFirstPage, hasMore, loading]);
|
||||||
|
|
||||||
// 主动检测内容高度 - 如果内容不足以填满容器,主动加载下一页
|
/**
|
||||||
|
* 【核心逻辑2】主动检测内容高度 - 确保内容始终填满容器
|
||||||
|
*
|
||||||
|
* 场景:
|
||||||
|
* - 初次加载时,如果 30 条数据不足以填满 800px 容器(例如显示器很大)
|
||||||
|
* - 用户无法滚动,也就无法触发上面的滚动监听逻辑
|
||||||
|
*
|
||||||
|
* 解决方案:
|
||||||
|
* - 定时检查 scrollHeight 是否小于等于 clientHeight
|
||||||
|
* - 如果内容不足,主动调用 loadNextPage() 加载更多数据
|
||||||
|
* - 递归触发,直到内容高度超过容器高度(出现滚动条)
|
||||||
|
*
|
||||||
|
* 优化:
|
||||||
|
* - 500ms 延迟:确保虚拟滚动已完成首次渲染和高度测量
|
||||||
|
* - 监听 events.length 变化:新数据加载后重新检查
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollElement = parentRef.current;
|
const scrollElement = parentRef.current;
|
||||||
if (!scrollElement || !loadNextPage) return;
|
if (!scrollElement || !loadNextPage) return;
|
||||||
@@ -133,36 +182,6 @@ const VirtualizedFourRowGrid = ({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [events.length, hasMore, loading, loadNextPage]);
|
}, [events.length, hasMore, loading, loadNextPage]);
|
||||||
|
|
||||||
// 滚动位置保持 - 加载上一页后,调整 scrollTop 使用户看到的内容位置不变
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollElement = parentRef.current;
|
|
||||||
if (!scrollElement || previousScrollHeight.current === 0) return;
|
|
||||||
|
|
||||||
// 延迟执行,确保虚拟滚动已重新渲染并测量了新高度
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const currentScrollHeight = scrollElement.scrollHeight;
|
|
||||||
const heightDifference = currentScrollHeight - previousScrollHeight.current;
|
|
||||||
|
|
||||||
// 如果高度增加了(说明上一页数据已加载),调整滚动位置
|
|
||||||
if (heightDifference > 0) {
|
|
||||||
console.log('%c📜 [位置保持] 调整滚动位置', 'color: #10B981; font-weight: bold;', {
|
|
||||||
previousHeight: previousScrollHeight.current,
|
|
||||||
currentHeight: currentScrollHeight,
|
|
||||||
heightDifference,
|
|
||||||
newScrollTop: scrollElement.scrollTop + heightDifference
|
|
||||||
});
|
|
||||||
|
|
||||||
// 调整 scrollTop,使用户看到的内容位置不变
|
|
||||||
scrollElement.scrollTop += heightDifference;
|
|
||||||
|
|
||||||
// 重置记录
|
|
||||||
previousScrollHeight.current = 0;
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [events.length]); // 监听 events 变化,加载上一页后会增加 events 数量
|
|
||||||
|
|
||||||
// 错误指示器(同行显示)
|
// 错误指示器(同行显示)
|
||||||
const renderErrorIndicator = () => {
|
const renderErrorIndicator = () => {
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
|
|||||||
@@ -2,9 +2,26 @@
|
|||||||
// 动态新闻卡片组件 - 常量配置
|
// 动态新闻卡片组件 - 常量配置
|
||||||
|
|
||||||
// ========== 分页配置常量 ==========
|
// ========== 分页配置常量 ==========
|
||||||
|
/**
|
||||||
|
* 分页大小计算依据:
|
||||||
|
*
|
||||||
|
* 【四排模式 (FOUR_ROW_PAGE_SIZE)】
|
||||||
|
* - 容器高度: 800px (VirtualizedFourRowGrid)
|
||||||
|
* - 单行高度: ~250px (包含卡片 + 间距)
|
||||||
|
* - 每行显示: 4 列
|
||||||
|
* - 可视区域: 800px / 250px ≈ 3.2 行
|
||||||
|
* - overscan 缓冲: 2 行 (上下各预渲染1行)
|
||||||
|
* - 实际渲染区域: 3.2 + 2 = 5.2 行
|
||||||
|
* - 单次加载数据量: 7.5 行 × 4 列 = 30 个
|
||||||
|
* - 设计目标: 提供充足的缓冲数据,确保快速滚动时不出现空白
|
||||||
|
*
|
||||||
|
* 【纵向模式 (VERTICAL_PAGE_SIZE)】
|
||||||
|
* - 每页显示 10 条数据
|
||||||
|
* - 使用传统分页器,用户手动翻页
|
||||||
|
*/
|
||||||
export const PAGINATION_CONFIG = {
|
export const PAGINATION_CONFIG = {
|
||||||
FOUR_ROW_PAGE_SIZE: 20, // 平铺模式每页数量
|
FOUR_ROW_PAGE_SIZE: 30, // 平铺模式每页数量 (7.5行 × 4列,包含缓冲)
|
||||||
VERTICAL_PAGE_SIZE: 10, // 纵向模式每页数量
|
VERTICAL_PAGE_SIZE: 10, // 纵向模式每页数量 (传统分页)
|
||||||
INITIAL_PAGE: 1, // 初始页码
|
INITIAL_PAGE: 1, // 初始页码
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user