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:
11
src/App.js
11
src/App.js
@@ -46,7 +46,6 @@ import { store } from './store';
|
|||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import { AuthProvider } from "contexts/AuthContext";
|
import { AuthProvider } from "contexts/AuthContext";
|
||||||
import { AuthModalProvider } from "contexts/AuthModalContext";
|
|
||||||
import { NotificationProvider, useNotification } from "contexts/NotificationContext";
|
import { NotificationProvider, useNotification } from "contexts/NotificationContext";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -319,12 +318,10 @@ export default function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AuthModalProvider>
|
<AppContent />
|
||||||
<AppContent />
|
<AuthModalManager />
|
||||||
<AuthModalManager />
|
<NotificationContainer />
|
||||||
<NotificationContainer />
|
<NotificationTestTool />
|
||||||
<NotificationTestTool />
|
|
||||||
</AuthModalProvider>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { FaLock, FaWeixin } from "react-icons/fa";
|
import { FaLock, FaWeixin } from "react-icons/fa";
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
import { useAuth } from "../../contexts/AuthContext";
|
||||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||||
import { useNotification } from "../../contexts/NotificationContext";
|
import { useNotification } from "../../contexts/NotificationContext";
|
||||||
import { authService } from "../../services/authService";
|
import { authService } from "../../services/authService";
|
||||||
import AuthHeader from './AuthHeader';
|
import AuthHeader from './AuthHeader';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
useBreakpointValue
|
useBreakpointValue
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
import { useAuthModal } from '../../hooks/useAuthModal';
|
||||||
import AuthFormContent from './AuthFormContent';
|
import AuthFormContent from './AuthFormContent';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { FaQrcode } from "react-icons/fa";
|
|||||||
import { FiAlertCircle } from "react-icons/fi";
|
import { FiAlertCircle } from "react-icons/fi";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/authService";
|
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 { useAuth } from "../../contexts/AuthContext";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { useAuthEvents } from "../../hooks/useAuthEvents";
|
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 { FaCrown } from 'react-icons/fa';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
import { useAuthModal } from '../../hooks/useAuthModal';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getApiBase } from '../../utils/apiConfig';
|
import { getApiBase } from '../../utils/apiConfig';
|
||||||
import SubscriptionButton from '../Subscription/SubscriptionButton';
|
import SubscriptionButton from '../Subscription/SubscriptionButton';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Box, VStack, Spinner, Text } from '@chakra-ui/react';
|
import { Box, VStack, Spinner, Text } from '@chakra-ui/react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useAuthModal } from '../contexts/AuthModalContext';
|
import { useAuthModal } from '../hooks/useAuthModal';
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }) => {
|
const ProtectedRoute = ({ children }) => {
|
||||||
const { isAuthenticated, isLoading, user } = useAuth();
|
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 posthogReducer from './slices/posthogSlice';
|
||||||
import industryReducer from './slices/industrySlice';
|
import industryReducer from './slices/industrySlice';
|
||||||
import stockReducer from './slices/stockSlice';
|
import stockReducer from './slices/stockSlice';
|
||||||
|
import authModalReducer from './slices/authModalSlice';
|
||||||
import posthogMiddleware from './middleware/posthogMiddleware';
|
import posthogMiddleware from './middleware/posthogMiddleware';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
@@ -12,6 +13,7 @@ export const store = configureStore({
|
|||||||
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
|
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
|
||||||
industry: industryReducer, // ✅ 行业分类数据管理
|
industry: industryReducer, // ✅ 行业分类数据管理
|
||||||
stock: stockReducer, // ✅ 股票和事件数据管理
|
stock: stockReducer, // ✅ 股票和事件数据管理
|
||||||
|
authModal: authModalReducer, // ✅ 认证弹窗状态管理
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
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