fix: refactor: 简化 Redux 数据管理逻辑并修复 bug

修复 clearCache/clearSpecificCache 引用不存在的 state.dynamicNews bug
简化数据插入逻辑,移除复杂的 Append/Replace/Jump 模式(虚拟滚动接管)
添加空数据边界处理和 toast 提示
添加 mode 参数支持(vertical/four-row)
修复默认值解构避免 undefined 错误
修复 Redux slice 未使用参数的 TS 警告 仅 preloadData 和 toggleEventFollow.rejected 的参数修改 将未使用的 state 参数改为 _state 前缀,消除 TS6133 警告
This commit is contained in:
zdl
2025-11-05 19:00:53 +08:00
parent c5dcb4897d
commit 2355004dfb

View File

@@ -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, // 服务端总数量
// 纵向模式数据(独立存储)
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 更新不同的 stateverticalEvents 或 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 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 existingIds = new Set(state[stateKey].map(e => e.id));
const newEvents = events.filter(e => !existingIds.has(e.id));
state.dynamicNews = [...state.dynamicNews, ...newEvents];
state[stateKey] = [...state[stateKey], ...newEvents];
logger.debug('CommunityData', '连续追加数据(去重', {
logger.debug('CommunityData', `追加数据(去重${mode}`, {
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
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
});
// 工具函数:检查数据是否需要刷新(超过指定时间)