feat: 将 AuthModalProvider 迁移到 Redux
## 主要改动 ### 新增 - 创建 `store/slices/authModalSlice.js` - Redux Slice 管理认证弹窗状态 - 创建 `hooks/useAuthModal.js` - 自定义 Hook,组合 Redux 状态和业务逻辑 ### 修改 - 更新 `store/index.js` - 添加 authModal reducer - 更新 `App.js` - 移除 AuthModalProvider 包裹层 - 更新 5 个组件的 import 路径: - AuthFormContent.js - AuthModalManager.js - WechatRegister.js - HomeNavbar.js - ProtectedRoute.js ### 删除 - 删除 `contexts/AuthModalContext.js` - 旧的 Context 实现 ## 迁移效果 - ✅ 减少 Provider 嵌套层级(4层 → 3层) - ✅ 统一状态管理架构(Redux) - ✅ 更好的调试体验(Redux DevTools) - ✅ 保持 API 兼容性(无破坏性修改) ## 技术细节 - 使用 `useRef` 存储 `onSuccessCallback`(函数不可序列化) - 保持与 AuthContext 的依赖关系(AuthProvider 暂未迁移) - 所有业务逻辑保持不变,仅改变状态管理方式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,6 @@ import { store } from './store';
|
||||
|
||||
// Contexts
|
||||
import { AuthProvider } from "contexts/AuthContext";
|
||||
import { AuthModalProvider } from "contexts/AuthModalContext";
|
||||
import { NotificationProvider, useNotification } from "contexts/NotificationContext";
|
||||
|
||||
// Components
|
||||
@@ -319,12 +318,10 @@ export default function App() {
|
||||
<ErrorBoundary>
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<AuthModalProvider>
|
||||
<AppContent />
|
||||
<AuthModalManager />
|
||||
<NotificationContainer />
|
||||
<NotificationTestTool />
|
||||
</AuthModalProvider>
|
||||
</AuthProvider>
|
||||
</NotificationProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { FaLock, FaWeixin } from "react-icons/fa";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||
import { useNotification } from "../../contexts/NotificationContext";
|
||||
import { authService } from "../../services/authService";
|
||||
import AuthHeader from './AuthHeader';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ModalCloseButton,
|
||||
useBreakpointValue
|
||||
} from '@chakra-ui/react';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import { useAuthModal } from '../../hooks/useAuthModal';
|
||||
import AuthFormContent from './AuthFormContent';
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ import { FaQrcode } from "react-icons/fa";
|
||||
import { FiAlertCircle } from "react-icons/fi";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { useAuthEvents } from "../../hooks/useAuthEvents";
|
||||
|
||||
@@ -44,7 +44,7 @@ import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-
|
||||
import { FaCrown } from 'react-icons/fa';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||
import { useAuthModal } from '../../hooks/useAuthModal';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
import SubscriptionButton from '../Subscription/SubscriptionButton';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Box, VStack, Spinner, Text } from '@chakra-ui/react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useAuthModal } from '../contexts/AuthModalContext';
|
||||
import { useAuthModal } from '../hooks/useAuthModal';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, isLoading, user } = useAuth();
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// src/contexts/AuthModalContext.js
|
||||
import { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const AuthModalContext = createContext();
|
||||
|
||||
/**
|
||||
* 自定义Hook:获取弹窗上下文
|
||||
*/
|
||||
export const useAuthModal = () => {
|
||||
const context = useContext(AuthModalContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuthModal must be used within AuthModalProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证弹窗提供者组件
|
||||
* 管理统一的认证弹窗状态(登录/注册合并)
|
||||
*/
|
||||
export const AuthModalProvider = ({ children }) => {
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
// 弹窗状态(统一的认证弹窗)
|
||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||
|
||||
// 重定向URL(认证成功后跳转)
|
||||
const [redirectUrl, setRedirectUrl] = useState(null);
|
||||
|
||||
// 成功回调函数
|
||||
const [onSuccessCallback, setOnSuccessCallback] = useState(null);
|
||||
|
||||
/**
|
||||
* 打开认证弹窗(统一的登录/注册入口)
|
||||
* @param {string} url - 认证成功后的重定向URL(可选)
|
||||
* @param {function} callback - 认证成功后的回调函数(可选)
|
||||
*/
|
||||
const openAuthModal = useCallback((url = null, callback = null) => {
|
||||
setRedirectUrl(url);
|
||||
setOnSuccessCallback(() => callback);
|
||||
setIsAuthModalOpen(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 关闭认证弹窗
|
||||
* 如果用户未登录,跳转到首页
|
||||
*/
|
||||
const closeModal = useCallback(() => {
|
||||
setIsAuthModalOpen(false);
|
||||
setRedirectUrl(null);
|
||||
setOnSuccessCallback(null);
|
||||
|
||||
// ⭐ 如果用户关闭弹窗时仍未登录,跳转到首页
|
||||
if (!isAuthenticated) {
|
||||
navigate('/home');
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
/**
|
||||
* 登录/注册成功处理
|
||||
* @param {object} user - 用户信息
|
||||
*/
|
||||
const handleLoginSuccess = useCallback((user) => {
|
||||
// 执行自定义回调(如果有)
|
||||
if (onSuccessCallback) {
|
||||
try {
|
||||
onSuccessCallback(user);
|
||||
} catch (error) {
|
||||
logger.error('AuthModalContext', 'handleLoginSuccess', error, {
|
||||
userId: user?.id,
|
||||
hasCallback: !!onSuccessCallback
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ 登录成功后,只关闭弹窗,留在当前页面(不跳转)
|
||||
// 移除了原有的 redirectUrl 跳转逻辑
|
||||
setIsAuthModalOpen(false);
|
||||
setRedirectUrl(null);
|
||||
setOnSuccessCallback(null);
|
||||
}, [onSuccessCallback]);
|
||||
|
||||
/**
|
||||
* 提供给子组件的上下文值
|
||||
*/
|
||||
const value = {
|
||||
// 状态
|
||||
isAuthModalOpen,
|
||||
redirectUrl,
|
||||
|
||||
// 打开弹窗方法
|
||||
openAuthModal,
|
||||
|
||||
// 关闭弹窗方法
|
||||
closeModal,
|
||||
|
||||
// 成功处理方法
|
||||
handleLoginSuccess,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthModalContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthModalContext.Provider>
|
||||
);
|
||||
};
|
||||
116
src/hooks/useAuthModal.js
Normal file
116
src/hooks/useAuthModal.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// src/hooks/useAuthModal.js
|
||||
// 认证弹窗自定义 Hook - 组合 Redux 状态和业务逻辑
|
||||
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
openModal,
|
||||
closeModal,
|
||||
selectAuthModalOpen,
|
||||
selectRedirectUrl
|
||||
} from '../store/slices/authModalSlice';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* 认证弹窗自定义 Hook
|
||||
*
|
||||
* 功能:
|
||||
* - 管理认证弹窗的开关状态
|
||||
* - 处理登录成功后的回调和跳转
|
||||
* - 未登录时关闭弹窗自动跳转到首页
|
||||
*
|
||||
* 注意:
|
||||
* - onSuccessCallback 使用 ref 存储(函数不可序列化,不能放 Redux)
|
||||
* - 依赖 AuthContext 读取 isAuthenticated(AuthProvider 暂未迁移)
|
||||
*
|
||||
* @returns {object} 弹窗状态和操作方法
|
||||
*/
|
||||
export const useAuthModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Redux 状态
|
||||
const isAuthModalOpen = useSelector(selectAuthModalOpen);
|
||||
const redirectUrl = useSelector(selectRedirectUrl);
|
||||
|
||||
// AuthContext 状态(暂未迁移到 Redux)
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
// 使用 ref 存储回调函数(不能放 Redux,因为函数不可序列化)
|
||||
const onSuccessCallbackRef = useRef(null);
|
||||
|
||||
/**
|
||||
* 打开认证弹窗(统一的登录/注册入口)
|
||||
* @param {string} url - 认证成功后的重定向URL(可选)
|
||||
* @param {function} callback - 认证成功后的回调函数(可选)
|
||||
*/
|
||||
const openAuthModal = useCallback((url = null, callback = null) => {
|
||||
onSuccessCallbackRef.current = callback;
|
||||
dispatch(openModal({ redirectUrl: url }));
|
||||
|
||||
logger.debug('useAuthModal', '打开认证弹窗', {
|
||||
redirectUrl: url || '无',
|
||||
hasCallback: !!callback
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* 关闭认证弹窗
|
||||
* 如果用户未登录,跳转到首页
|
||||
*/
|
||||
const closeAuthModal = useCallback(() => {
|
||||
dispatch(closeModal());
|
||||
onSuccessCallbackRef.current = null;
|
||||
|
||||
// ⭐ 如果用户关闭弹窗时仍未登录,跳转到首页
|
||||
if (!isAuthenticated) {
|
||||
navigate('/home');
|
||||
logger.debug('useAuthModal', '未登录关闭弹窗,跳转到首页');
|
||||
} else {
|
||||
logger.debug('useAuthModal', '关闭认证弹窗');
|
||||
}
|
||||
}, [dispatch, isAuthenticated, navigate]);
|
||||
|
||||
/**
|
||||
* 登录/注册成功处理
|
||||
* @param {object} user - 用户信息
|
||||
*/
|
||||
const handleLoginSuccess = useCallback((user) => {
|
||||
// 执行自定义回调(如果有)
|
||||
if (onSuccessCallbackRef.current) {
|
||||
try {
|
||||
onSuccessCallbackRef.current(user);
|
||||
logger.debug('useAuthModal', '执行成功回调', {
|
||||
userId: user?.id
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('useAuthModal', 'handleLoginSuccess 回调执行失败', error, {
|
||||
userId: user?.id,
|
||||
hasCallback: !!onSuccessCallbackRef.current
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ 登录成功后,只关闭弹窗,留在当前页面(不跳转)
|
||||
// 移除了原有的 redirectUrl 跳转逻辑
|
||||
dispatch(closeModal());
|
||||
onSuccessCallbackRef.current = null;
|
||||
|
||||
logger.debug('useAuthModal', '登录成功,关闭弹窗', {
|
||||
userId: user?.id
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isAuthModalOpen,
|
||||
redirectUrl,
|
||||
|
||||
// 方法
|
||||
openAuthModal,
|
||||
closeModal: closeAuthModal,
|
||||
handleLoginSuccess,
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import communityDataReducer from './slices/communityDataSlice';
|
||||
import posthogReducer from './slices/posthogSlice';
|
||||
import industryReducer from './slices/industrySlice';
|
||||
import stockReducer from './slices/stockSlice';
|
||||
import authModalReducer from './slices/authModalSlice';
|
||||
import posthogMiddleware from './middleware/posthogMiddleware';
|
||||
|
||||
export const store = configureStore({
|
||||
@@ -12,6 +13,7 @@ export const store = configureStore({
|
||||
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
|
||||
industry: industryReducer, // ✅ 行业分类数据管理
|
||||
stock: stockReducer, // ✅ 股票和事件数据管理
|
||||
authModal: authModalReducer, // ✅ 认证弹窗状态管理
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
|
||||
56
src/store/slices/authModalSlice.js
Normal file
56
src/store/slices/authModalSlice.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// src/store/slices/authModalSlice.js
|
||||
// 认证弹窗状态管理 Redux Slice - 从 AuthModalContext 迁移
|
||||
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* AuthModal Slice
|
||||
* 管理统一的认证弹窗状态(登录/注册合并)
|
||||
*/
|
||||
const authModalSlice = createSlice({
|
||||
name: 'authModal',
|
||||
initialState: {
|
||||
isOpen: false, // 弹窗开关状态
|
||||
redirectUrl: null, // 认证成功后的重定向URL(可选)
|
||||
},
|
||||
reducers: {
|
||||
/**
|
||||
* 打开认证弹窗
|
||||
* @param {object} action.payload - { redirectUrl?: string }
|
||||
*/
|
||||
openModal: (state, action) => {
|
||||
state.isOpen = true;
|
||||
state.redirectUrl = action.payload?.redirectUrl || null;
|
||||
logger.debug('authModalSlice', '打开认证弹窗', {
|
||||
redirectUrl: action.payload?.redirectUrl || '无'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭认证弹窗
|
||||
*/
|
||||
closeModal: (state) => {
|
||||
state.isOpen = false;
|
||||
state.redirectUrl = null;
|
||||
logger.debug('authModalSlice', '关闭认证弹窗');
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置重定向URL(不打开弹窗)
|
||||
*/
|
||||
setRedirectUrl: (state, action) => {
|
||||
state.redirectUrl = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const { openModal, closeModal, setRedirectUrl } = authModalSlice.actions;
|
||||
|
||||
// 导出 selectors
|
||||
export const selectAuthModalOpen = (state) => state.authModal.isOpen;
|
||||
export const selectRedirectUrl = (state) => state.authModal.redirectUrl;
|
||||
|
||||
// 导出 reducer
|
||||
export default authModalSlice.reducer;
|
||||
Reference in New Issue
Block a user