feat: 实现 Redux 全局状态管理事件关注功能
本次提交实现了滚动列表和事件详情的关注按钮状态同步: ✅ Redux 状态管理 - communityDataSlice.js: 添加 eventFollowStatus state - 新增 toggleEventFollow AsyncThunk(复用 EventList.js 逻辑) - 新增 setEventFollowStatus reducer 和 selectEventFollowStatus selector ✅ 组件集成 - DynamicNewsCard.js: 从 Redux 读取关注状态并传递给子组件 - EventScrollList.js: 接收并传递关注状态给事件卡片 - DynamicNewsDetailPanel.js: 移除本地 state,使用 Redux 状态 ✅ Mock API 支持 - event.js: 添加 POST /api/events/:eventId/follow 处理器 - 返回 { is_following, follower_count } 模拟数据 ✅ Bug 修复 - EventDetail/index.js: 添加 useRef 导入 - concept.js: 导出 generatePopularConcepts 函数 - event.js: 添加 /api/events/:eventId/concepts 处理器 功能: - 点击滚动列表的关注按钮,详情面板的关注状态自动同步 - 点击详情面板的关注按钮,滚动列表的关注状态自动同步 - 关注人数实时更新 - 状态在整个应用中保持一致 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -157,15 +157,30 @@ export const fetchHotEvents = createAsyncThunk(
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取动态新闻(无缓存,每次都发起请求)
|
||||
* 用于 DynamicNewsCard 组件,需要保持实时性
|
||||
* @param {Object} params - 分页参数 { page, per_page }
|
||||
* 获取动态新闻(客户端缓存 + 智能请求)
|
||||
* 用于 DynamicNewsCard 组件
|
||||
* @param {Object} params - 请求参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.per_page - 每页数量
|
||||
* @param {boolean} params.clearCache - 是否清空缓存(默认 false)
|
||||
* @param {boolean} params.prependMode - 是否追加到头部(用于定时刷新,默认 false)
|
||||
*/
|
||||
export const fetchDynamicNews = createAsyncThunk(
|
||||
'communityData/fetchDynamicNews',
|
||||
async ({ page = 1, per_page = 5 } = {}, { rejectWithValue }) => {
|
||||
async ({
|
||||
page = 1,
|
||||
per_page = 5,
|
||||
clearCache = false,
|
||||
prependMode = false
|
||||
} = {}, { rejectWithValue }) => {
|
||||
try {
|
||||
logger.debug('CommunityData', '开始获取动态新闻', { page, per_page });
|
||||
logger.debug('CommunityData', '开始获取动态新闻', {
|
||||
page,
|
||||
per_page,
|
||||
clearCache,
|
||||
prependMode
|
||||
});
|
||||
|
||||
const response = await eventService.getEvents({
|
||||
page,
|
||||
per_page,
|
||||
@@ -180,12 +195,19 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
});
|
||||
return {
|
||||
events: response.data.events,
|
||||
pagination: response.data.pagination || {}
|
||||
total: response.data.pagination?.total || 0,
|
||||
clearCache,
|
||||
prependMode
|
||||
};
|
||||
}
|
||||
|
||||
logger.warn('CommunityData', '动态新闻返回数据为空', response);
|
||||
return { events: [], pagination: {} };
|
||||
return {
|
||||
events: [],
|
||||
total: 0,
|
||||
clearCache,
|
||||
prependMode
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('CommunityData', '获取动态新闻失败', error);
|
||||
return rejectWithValue(error.message || '获取动态新闻失败');
|
||||
@@ -193,6 +215,51 @@ export const fetchDynamicNews = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 切换事件关注状态
|
||||
* 复用 EventList.js 中的关注逻辑
|
||||
* @param {number} eventId - 事件ID
|
||||
*/
|
||||
export const toggleEventFollow = createAsyncThunk(
|
||||
'communityData/toggleEventFollow',
|
||||
async (eventId, { rejectWithValue }) => {
|
||||
try {
|
||||
logger.debug('CommunityData', '切换事件关注状态', { eventId });
|
||||
|
||||
// 调用 API(自动切换关注状态,后端根据当前状态决定关注/取消关注)
|
||||
const response = await fetch(`/api/events/${eventId}/follow`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '操作失败');
|
||||
}
|
||||
|
||||
const isFollowing = data.data?.is_following;
|
||||
const followerCount = data.data?.follower_count ?? 0;
|
||||
|
||||
logger.info('CommunityData', '关注状态切换成功', {
|
||||
eventId,
|
||||
isFollowing,
|
||||
followerCount
|
||||
});
|
||||
|
||||
return {
|
||||
eventId,
|
||||
isFollowing,
|
||||
followerCount
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('CommunityData', '切换关注状态失败', error);
|
||||
return rejectWithValue(error.message || '切换关注状态失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== Slice 定义 ====================
|
||||
|
||||
const communityDataSlice = createSlice({
|
||||
@@ -201,8 +268,9 @@ const communityDataSlice = createSlice({
|
||||
// 数据
|
||||
popularKeywords: [],
|
||||
hotEvents: [],
|
||||
dynamicNews: [], // 动态新闻(无缓存)
|
||||
dynamicNewsPagination: {}, // 动态新闻分页信息
|
||||
dynamicNews: [], // 动态新闻完整缓存列表
|
||||
dynamicNewsTotal: 0, // 服务端总数量
|
||||
eventFollowStatus: {}, // 事件关注状态 { [eventId]: { isFollowing: boolean, followerCount: number } }
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
@@ -278,6 +346,16 @@ const communityDataSlice = createSlice({
|
||||
preloadData: (state) => {
|
||||
logger.info('CommunityData', '准备预加载数据');
|
||||
// 实际的预加载逻辑在组件中调用 dispatch(fetchPopularKeywords()) 等
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置单个事件的关注状态(同步)
|
||||
* @param {Object} action.payload - { eventId, isFollowing, followerCount }
|
||||
*/
|
||||
setEventFollowStatus: (state, action) => {
|
||||
const { eventId, isFollowing, followerCount } = action.payload;
|
||||
state.eventFollowStatus[eventId] = { isFollowing, followerCount };
|
||||
logger.debug('CommunityData', '设置事件关注状态', { eventId, isFollowing, followerCount });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -286,34 +364,71 @@ const communityDataSlice = createSlice({
|
||||
createDataReducers(builder, fetchPopularKeywords, 'popularKeywords');
|
||||
createDataReducers(builder, fetchHotEvents, 'hotEvents');
|
||||
|
||||
// dynamicNews 需要特殊处理(包含 pagination)
|
||||
// dynamicNews 需要特殊处理(缓存 + 追加模式)
|
||||
builder
|
||||
.addCase(fetchDynamicNews.pending, (state) => {
|
||||
state.loading.dynamicNews = true;
|
||||
state.error.dynamicNews = null;
|
||||
})
|
||||
.addCase(fetchDynamicNews.fulfilled, (state, action) => {
|
||||
const { events, total, clearCache, prependMode } = action.payload;
|
||||
|
||||
if (clearCache) {
|
||||
// 清空缓存模式:直接替换
|
||||
state.dynamicNews = events;
|
||||
logger.debug('CommunityData', '清空缓存并加载新数据', {
|
||||
count: events.length
|
||||
});
|
||||
} else if (prependMode) {
|
||||
// 追加到头部模式(用于定时刷新):去重后插入头部
|
||||
const existingIds = new Set(state.dynamicNews.map(e => e.id));
|
||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||
state.dynamicNews = [...newEvents, ...state.dynamicNews];
|
||||
logger.debug('CommunityData', '追加新数据到头部', {
|
||||
newCount: newEvents.length,
|
||||
totalCount: state.dynamicNews.length
|
||||
});
|
||||
} else {
|
||||
// 追加到尾部模式(默认):去重后追加
|
||||
const existingIds = new Set(state.dynamicNews.map(e => e.id));
|
||||
const newEvents = events.filter(e => !existingIds.has(e.id));
|
||||
state.dynamicNews = [...state.dynamicNews, ...newEvents];
|
||||
logger.debug('CommunityData', '追加新数据到尾部', {
|
||||
newCount: newEvents.length,
|
||||
totalCount: state.dynamicNews.length
|
||||
});
|
||||
}
|
||||
|
||||
state.dynamicNewsTotal = total;
|
||||
state.loading.dynamicNews = false;
|
||||
state.dynamicNews = action.payload.events;
|
||||
state.dynamicNewsPagination = action.payload.pagination;
|
||||
state.lastUpdated.dynamicNews = 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));
|
||||
})
|
||||
// toggleEventFollow
|
||||
.addCase(toggleEventFollow.fulfilled, (state, action) => {
|
||||
const { eventId, isFollowing, followerCount } = action.payload;
|
||||
state.eventFollowStatus[eventId] = { isFollowing, followerCount };
|
||||
logger.debug('CommunityData', 'toggleEventFollow fulfilled', { eventId, isFollowing, followerCount });
|
||||
})
|
||||
.addCase(toggleEventFollow.rejected, (state, action) => {
|
||||
logger.error('CommunityData', 'toggleEventFollow rejected', action.payload);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 导出 ====================
|
||||
|
||||
export const { clearCache, clearSpecificCache, preloadData } = communityDataSlice.actions;
|
||||
export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus } = communityDataSlice.actions;
|
||||
|
||||
// 基础选择器(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;
|
||||
@@ -334,10 +449,11 @@ export const selectHotEventsWithLoading = (state) => ({
|
||||
});
|
||||
|
||||
export const selectDynamicNewsWithLoading = (state) => ({
|
||||
data: state.communityData.dynamicNews,
|
||||
data: state.communityData.dynamicNews, // 完整缓存列表
|
||||
loading: state.communityData.loading.dynamicNews,
|
||||
error: state.communityData.error.dynamicNews,
|
||||
pagination: state.communityData.dynamicNewsPagination,
|
||||
total: state.communityData.dynamicNewsTotal, // 服务端总数量
|
||||
cachedCount: state.communityData.dynamicNews.length, // 已缓存数量
|
||||
lastUpdated: state.communityData.lastUpdated.dynamicNews
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user