diff --git a/src/App.js b/src/App.js index 7ff206da..bb7b11fb 100755 --- a/src/App.js +++ b/src/App.js @@ -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() { - - - - - - + + + + diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index 8435c597..6e808ddf 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -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'; diff --git a/src/components/Auth/AuthModalManager.js b/src/components/Auth/AuthModalManager.js index 40e38cd6..d1314ea4 100644 --- a/src/components/Auth/AuthModalManager.js +++ b/src/components/Auth/AuthModalManager.js @@ -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'; /** diff --git a/src/components/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index 1c144ae5..a6f38dec 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -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"; diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index 5a1dc1f4..55b3ce0c 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -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'; diff --git a/src/components/ProtectedRoute.js b/src/components/ProtectedRoute.js index a415a172..cb9716f0 100755 --- a/src/components/ProtectedRoute.js +++ b/src/components/ProtectedRoute.js @@ -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(); diff --git a/src/contexts/AuthModalContext.js b/src/contexts/AuthModalContext.js deleted file mode 100644 index 1269619f..00000000 --- a/src/contexts/AuthModalContext.js +++ /dev/null @@ -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 ( - - {children} - - ); -}; diff --git a/src/hooks/useAuthModal.js b/src/hooks/useAuthModal.js new file mode 100644 index 00000000..956d5d24 --- /dev/null +++ b/src/hooks/useAuthModal.js @@ -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, + }; +}; diff --git a/src/store/index.js b/src/store/index.js index d983721f..9adafda6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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({ diff --git a/src/store/slices/authModalSlice.js b/src/store/slices/authModalSlice.js new file mode 100644 index 00000000..e8ff1696 --- /dev/null +++ b/src/store/slices/authModalSlice.js @@ -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;