feat: 热门关键词UI调整 数据获取逻辑调整 接入redux
This commit is contained in:
18
src/store/index.js
Normal file
18
src/store/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// src/store/index.js
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import communityDataReducer from './slices/communityDataSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
communityData: communityDataReducer
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
// 忽略这些 action types 的序列化检查
|
||||
ignoredActions: ['communityData/fetchPopularKeywords/fulfilled', 'communityData/fetchHotEvents/fulfilled'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export default store;
|
||||
321
src/store/slices/communityDataSlice.js
Normal file
321
src/store/slices/communityDataSlice.js
Normal file
@@ -0,0 +1,321 @@
|
||||
// src/store/slices/communityDataSlice.js
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { eventService } from '../../services/eventService';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
// ==================== 常量定义 ====================
|
||||
|
||||
// 缓存键名
|
||||
const CACHE_KEYS = {
|
||||
POPULAR_KEYWORDS: 'community_popular_keywords',
|
||||
HOT_EVENTS: 'community_hot_events'
|
||||
};
|
||||
|
||||
// 缓存有效期类型
|
||||
const CACHE_EXPIRY_TYPE = {
|
||||
MIDNIGHT: 'midnight', // 当天午夜过期
|
||||
HOURS: 'hours' // 指定小时数后过期
|
||||
};
|
||||
|
||||
// ==================== 缓存工具函数 ====================
|
||||
|
||||
/**
|
||||
* 计算缓存过期时间
|
||||
* @param {string} type - 过期类型 (midnight/hours)
|
||||
* @param {number} hours - 小时数(仅当 type 为 hours 时使用)
|
||||
* @returns {string} ISO 格式的过期时间
|
||||
*/
|
||||
const getExpireTime = (type = CACHE_EXPIRY_TYPE.MIDNIGHT, hours = 24) => {
|
||||
const expireDate = new Date();
|
||||
|
||||
if (type === CACHE_EXPIRY_TYPE.MIDNIGHT) {
|
||||
// 设置为明天凌晨 0 点
|
||||
expireDate.setDate(expireDate.getDate() + 1);
|
||||
expireDate.setHours(0, 0, 0, 0);
|
||||
} else {
|
||||
// 设置为指定小时后
|
||||
expireDate.setHours(expireDate.getHours() + hours);
|
||||
}
|
||||
|
||||
return expireDate.toISOString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 localStorage 获取缓存数据
|
||||
* @param {string} key - 缓存键名
|
||||
* @returns {any|null} 缓存的数据或 null
|
||||
*/
|
||||
const getCachedData = (key) => {
|
||||
try {
|
||||
const cached = localStorage.getItem(key);
|
||||
if (!cached) return null;
|
||||
|
||||
const { data, expireAt } = JSON.parse(cached);
|
||||
|
||||
// 检查是否过期
|
||||
if (new Date() > new Date(expireAt)) {
|
||||
localStorage.removeItem(key);
|
||||
logger.debug('CommunityData', '缓存已过期', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug('CommunityData', '使用缓存数据', {
|
||||
key,
|
||||
dataLength: Array.isArray(data) ? data.length : 'N/A',
|
||||
expireAt
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('CommunityData', 'getCachedData 失败', error, { key });
|
||||
// 清除损坏的缓存
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存数据到 localStorage
|
||||
* @param {string} key - 缓存键名
|
||||
* @param {any} data - 要缓存的数据
|
||||
* @param {string} expiryType - 过期类型
|
||||
*/
|
||||
const setCachedData = (key, data, expiryType = CACHE_EXPIRY_TYPE.MIDNIGHT) => {
|
||||
try {
|
||||
const cacheData = {
|
||||
data,
|
||||
expireAt: getExpireTime(expiryType),
|
||||
cachedAt: new Date().toISOString()
|
||||
};
|
||||
localStorage.setItem(key, JSON.stringify(cacheData));
|
||||
logger.debug('CommunityData', '数据已缓存', {
|
||||
key,
|
||||
dataLength: Array.isArray(data) ? data.length : 'N/A',
|
||||
expireAt: cacheData.expireAt,
|
||||
cachedAt: cacheData.cachedAt
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('CommunityData', 'setCachedData 失败', error, { key });
|
||||
// localStorage 可能已满,尝试清理
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
logger.warn('CommunityData', 'localStorage 配额已满,尝试清理旧缓存');
|
||||
Object.values(CACHE_KEYS).forEach(cacheKey => {
|
||||
localStorage.removeItem(cacheKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 通用数据获取逻辑 ====================
|
||||
|
||||
/**
|
||||
* 通用的数据获取函数(支持三级缓存:Redux -> localStorage -> API)
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {string} options.cacheKey - 缓存键名
|
||||
* @param {Function} options.fetchFn - API 获取函数
|
||||
* @param {Function} options.getState - Redux getState 函数
|
||||
* @param {string} options.stateKey - Redux state 中的键名
|
||||
* @param {boolean} options.forceRefresh - 是否强制刷新
|
||||
* @returns {Promise<any>} 获取的数据
|
||||
*/
|
||||
const fetchWithCache = async ({
|
||||
cacheKey,
|
||||
fetchFn,
|
||||
getState,
|
||||
stateKey,
|
||||
forceRefresh = false
|
||||
}) => {
|
||||
try {
|
||||
// 第一级缓存:检查 Redux 状态(除非强制刷新)
|
||||
if (!forceRefresh) {
|
||||
const stateData = getState().communityData[stateKey];
|
||||
if (stateData && stateData.length > 0) {
|
||||
logger.debug('CommunityData', `Redux 状态中已有${stateKey}数据`);
|
||||
return stateData;
|
||||
}
|
||||
|
||||
// 第二级缓存:检查 localStorage
|
||||
const cachedData = getCachedData(cacheKey);
|
||||
if (cachedData) {
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
|
||||
// 第三级:从 API 获取
|
||||
logger.debug('CommunityData', `从 API 获取${stateKey}`, { forceRefresh });
|
||||
const response = await fetchFn();
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 保存到 localStorage
|
||||
setCachedData(cacheKey, response.data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
logger.warn('CommunityData', `API 返回数据为空:${stateKey}`);
|
||||
return [];
|
||||
} catch (error) {
|
||||
logger.error('CommunityData', `获取${stateKey}失败`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Async Thunks ====================
|
||||
|
||||
/**
|
||||
* 获取热门关键词
|
||||
* @param {boolean} forceRefresh - 是否强制刷新(跳过缓存)
|
||||
*/
|
||||
export const fetchPopularKeywords = createAsyncThunk(
|
||||
'communityData/fetchPopularKeywords',
|
||||
async (forceRefresh = false, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
return await fetchWithCache({
|
||||
cacheKey: CACHE_KEYS.POPULAR_KEYWORDS,
|
||||
fetchFn: () => eventService.getPopularKeywords(20),
|
||||
getState,
|
||||
stateKey: 'popularKeywords',
|
||||
forceRefresh
|
||||
});
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message || '获取热门关键词失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取热点事件
|
||||
* @param {boolean} forceRefresh - 是否强制刷新(跳过缓存)
|
||||
*/
|
||||
export const fetchHotEvents = createAsyncThunk(
|
||||
'communityData/fetchHotEvents',
|
||||
async (forceRefresh = false, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
return await fetchWithCache({
|
||||
cacheKey: CACHE_KEYS.HOT_EVENTS,
|
||||
fetchFn: () => eventService.getHotEvents({ days: 5, limit: 4 }),
|
||||
getState,
|
||||
stateKey: 'hotEvents',
|
||||
forceRefresh
|
||||
});
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message || '获取热点事件失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== Slice 定义 ====================
|
||||
|
||||
const communityDataSlice = createSlice({
|
||||
name: 'communityData',
|
||||
initialState: {
|
||||
// 数据
|
||||
popularKeywords: [],
|
||||
hotEvents: [],
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
popularKeywords: false,
|
||||
hotEvents: false
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
error: {
|
||||
popularKeywords: null,
|
||||
hotEvents: null
|
||||
},
|
||||
|
||||
// 最后更新时间
|
||||
lastUpdated: {
|
||||
popularKeywords: null,
|
||||
hotEvents: null
|
||||
}
|
||||
},
|
||||
|
||||
reducers: {
|
||||
/**
|
||||
* 清除所有缓存(Redux + localStorage)
|
||||
*/
|
||||
clearCache: (state) => {
|
||||
// 清除 localStorage
|
||||
Object.values(CACHE_KEYS).forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
// 清除 Redux 状态
|
||||
state.popularKeywords = [];
|
||||
state.hotEvents = [];
|
||||
state.lastUpdated.popularKeywords = null;
|
||||
state.lastUpdated.hotEvents = null;
|
||||
|
||||
logger.info('CommunityData', '所有缓存已清除');
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除指定类型的缓存
|
||||
* @param {string} payload - 缓存类型 ('popularKeywords' | 'hotEvents')
|
||||
*/
|
||||
clearSpecificCache: (state, action) => {
|
||||
const type = action.payload;
|
||||
|
||||
if (type === 'popularKeywords') {
|
||||
localStorage.removeItem(CACHE_KEYS.POPULAR_KEYWORDS);
|
||||
state.popularKeywords = [];
|
||||
state.lastUpdated.popularKeywords = null;
|
||||
logger.info('CommunityData', '热门关键词缓存已清除');
|
||||
} else if (type === 'hotEvents') {
|
||||
localStorage.removeItem(CACHE_KEYS.HOT_EVENTS);
|
||||
state.hotEvents = [];
|
||||
state.lastUpdated.hotEvents = null;
|
||||
logger.info('CommunityData', '热点事件缓存已清除');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
extraReducers: (builder) => {
|
||||
// ===== 热门关键词 =====
|
||||
builder
|
||||
.addCase(fetchPopularKeywords.pending, (state) => {
|
||||
state.loading.popularKeywords = true;
|
||||
state.error.popularKeywords = null;
|
||||
})
|
||||
.addCase(fetchPopularKeywords.fulfilled, (state, action) => {
|
||||
state.loading.popularKeywords = false;
|
||||
state.popularKeywords = action.payload;
|
||||
state.lastUpdated.popularKeywords = new Date().toISOString();
|
||||
})
|
||||
.addCase(fetchPopularKeywords.rejected, (state, action) => {
|
||||
state.loading.popularKeywords = false;
|
||||
state.error.popularKeywords = action.payload;
|
||||
logger.error('CommunityData', '热门关键词加载失败', new Error(action.payload));
|
||||
});
|
||||
|
||||
// ===== 热点事件 =====
|
||||
builder
|
||||
.addCase(fetchHotEvents.pending, (state) => {
|
||||
state.loading.hotEvents = true;
|
||||
state.error.hotEvents = null;
|
||||
})
|
||||
.addCase(fetchHotEvents.fulfilled, (state, action) => {
|
||||
state.loading.hotEvents = false;
|
||||
state.hotEvents = action.payload;
|
||||
state.lastUpdated.hotEvents = new Date().toISOString();
|
||||
})
|
||||
.addCase(fetchHotEvents.rejected, (state, action) => {
|
||||
state.loading.hotEvents = false;
|
||||
state.error.hotEvents = action.payload;
|
||||
logger.error('CommunityData', '热点事件加载失败', new Error(action.payload));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 导出 ====================
|
||||
|
||||
export const { clearCache, clearSpecificCache } = communityDataSlice.actions;
|
||||
|
||||
// 选择器(Selectors)
|
||||
export const selectPopularKeywords = (state) => state.communityData.popularKeywords;
|
||||
export const selectHotEvents = (state) => state.communityData.hotEvents;
|
||||
export const selectLoading = (state) => state.communityData.loading;
|
||||
export const selectError = (state) => state.communityData.error;
|
||||
export const selectLastUpdated = (state) => state.communityData.lastUpdated;
|
||||
|
||||
export default communityDataSlice.reducer;
|
||||
Reference in New Issue
Block a user