refactor(subscription): Phase 2 - 迁移到 Redux 状态管理
重构目标: 使用 Redux 管理订阅数据,替代本地状态 Phase 2 完成: ✅ 创建 subscriptionSlice.js (143行) - Redux Toolkit createSlice + createAsyncThunk - 管理订阅信息、loading、error、Modal 状态 - fetchSubscriptionInfo 异步 thunk - resetToFree reducer (登出时调用) ✅ 注册到 Redux Store - 添加 subscriptionReducer 到 store ✅ 重构 useSubscription Hook (182行) - 从本地状态迁移到 Redux (useSelector + useDispatch) - 保留所有权限检查逻辑 - 新增: isSubscriptionModalOpen, open/closeSubscriptionModal - 自动加载订阅数据 (登录时) ✅ 重构 HomeNavbar 使用 Redux - 替换 useSubscriptionData → useSubscription - 删除 ./hooks/useSubscriptionData.js 架构优势: ✅ 全局状态共享 - 多组件可访问订阅数据 ✅ Redux DevTools 可调试 ✅ 异步逻辑统一管理 (createAsyncThunk) ✅ 与现有架构一致 (authModalSlice 等) 性能优化: ✅ Redux 状态优化,减少不必要渲染 ✅ useSelector 精确订阅,只在相关数据变化时更新 累计优化: - 原始: 1623行 - Phase 1后: 1573行 (↓ 50行) - Phase 2后: 1533行 (↓ 90行, -5.5%) - 新增 Redux 逻辑: subscriptionSlice (143行) + Hook (182行) 下一步: Phase 3+ 继续拆分组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
143
src/store/slices/subscriptionSlice.js
Normal file
143
src/store/slices/subscriptionSlice.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// src/store/slices/subscriptionSlice.js
|
||||
// 订阅信息状态管理 Redux Slice
|
||||
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
|
||||
/**
|
||||
* 异步 Thunk: 获取用户订阅信息
|
||||
*/
|
||||
export const fetchSubscriptionInfo = createAsyncThunk(
|
||||
'subscription/fetchInfo',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const base = getApiBase();
|
||||
logger.debug('subscriptionSlice', '开始加载订阅信息');
|
||||
|
||||
const response = await fetch(base + '/api/subscription/current', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 数据标准化处理
|
||||
const normalizedData = {
|
||||
type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
|
||||
status: data.data.status || 'active',
|
||||
days_left: data.data.days_left || 0,
|
||||
is_active: data.data.is_active !== false,
|
||||
end_date: data.data.end_date || null
|
||||
};
|
||||
|
||||
logger.info('subscriptionSlice', '订阅信息加载成功', normalizedData);
|
||||
return normalizedData;
|
||||
} else {
|
||||
// API 返回成功但无数据,返回默认免费版
|
||||
return {
|
||||
type: 'free',
|
||||
status: 'active',
|
||||
days_left: 0,
|
||||
is_active: false,
|
||||
end_date: null
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('subscriptionSlice', '加载订阅信息失败', error);
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscription Slice
|
||||
* 管理用户订阅信息和订阅 Modal 状态
|
||||
*/
|
||||
const subscriptionSlice = createSlice({
|
||||
name: 'subscription',
|
||||
initialState: {
|
||||
// 订阅信息
|
||||
info: {
|
||||
type: 'free',
|
||||
status: 'active',
|
||||
days_left: 0,
|
||||
is_active: false,
|
||||
end_date: null
|
||||
},
|
||||
// 加载状态
|
||||
loading: false,
|
||||
error: null,
|
||||
// 订阅 Modal 状态
|
||||
isModalOpen: false,
|
||||
},
|
||||
reducers: {
|
||||
/**
|
||||
* 打开订阅 Modal
|
||||
*/
|
||||
openModal: (state) => {
|
||||
state.isModalOpen = true;
|
||||
logger.debug('subscriptionSlice', '打开订阅 Modal');
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭订阅 Modal
|
||||
*/
|
||||
closeModal: (state) => {
|
||||
state.isModalOpen = false;
|
||||
logger.debug('subscriptionSlice', '关闭订阅 Modal');
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置为免费版 (用户登出时调用)
|
||||
*/
|
||||
resetToFree: (state) => {
|
||||
state.info = {
|
||||
type: 'free',
|
||||
status: 'active',
|
||||
days_left: 0,
|
||||
is_active: false,
|
||||
end_date: null
|
||||
};
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
logger.debug('subscriptionSlice', '重置订阅信息为免费版');
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// fetchSubscriptionInfo - pending
|
||||
.addCase(fetchSubscriptionInfo.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
// fetchSubscriptionInfo - fulfilled
|
||||
.addCase(fetchSubscriptionInfo.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.info = action.payload;
|
||||
state.error = null;
|
||||
})
|
||||
// fetchSubscriptionInfo - rejected
|
||||
.addCase(fetchSubscriptionInfo.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload || 'Unknown error';
|
||||
// 加载失败时保持当前状态,不重置为免费版
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const { openModal, closeModal, resetToFree } = subscriptionSlice.actions;
|
||||
|
||||
// 导出 selectors
|
||||
export const selectSubscriptionInfo = (state) => state.subscription.info;
|
||||
export const selectSubscriptionLoading = (state) => state.subscription.loading;
|
||||
export const selectSubscriptionError = (state) => state.subscription.error;
|
||||
export const selectSubscriptionModalOpen = (state) => state.subscription.isModalOpen;
|
||||
|
||||
// 导出 reducer
|
||||
export default subscriptionSlice.reducer;
|
||||
Reference in New Issue
Block a user