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:
zdl
2025-10-30 16:50:10 +08:00
parent 5387b2d032
commit e5205ce097
4 changed files with 309 additions and 246 deletions

View File

@@ -5,6 +5,7 @@ import posthogReducer from './slices/posthogSlice';
import industryReducer from './slices/industrySlice';
import stockReducer from './slices/stockSlice';
import authModalReducer from './slices/authModalSlice';
import subscriptionReducer from './slices/subscriptionSlice';
import posthogMiddleware from './middleware/posthogMiddleware';
export const store = configureStore({
@@ -14,6 +15,7 @@ export const store = configureStore({
industry: industryReducer, // ✅ 行业分类数据管理
stock: stockReducer, // ✅ 股票和事件数据管理
authModal: authModalReducer, // ✅ 认证弹窗状态管理
subscription: subscriptionReducer, // ✅ 订阅信息状态管理
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({

View 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;