From e91656d33211dff299b59dfb8192545925d48a96 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 24 Oct 2025 12:19:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20user=20=E4=BE=9D=E8=B5=96=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Navbars/HomeNavbar.js | 105 ++++++++++++++++----------- src/components/ProtectedRoute.js | 12 +-- src/contexts/AuthContext.js | 37 +++++++--- 3 files changed, 97 insertions(+), 57 deletions(-) diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index def40305..689b84cd 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -489,6 +489,11 @@ export default function HomeNavbar() { const brandHover = useColorModeValue('blue.600', 'blue.300'); const toast = useToast(); + // ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环 + const userId = user?.id; + const prevUserIdRef = React.useRef(userId); + const prevIsAuthenticatedRef = React.useRef(isAuthenticated); + // 添加调试信息 logger.debug('HomeNavbar', '组件渲染状态', { hasUser: !!user, @@ -727,65 +732,81 @@ export default function HomeNavbar() { error: error.message }); } - }, [isAuthenticated, user?.id]); // 只依赖 user.id,避免 user 对象变化导致无限循环 + }, [isAuthenticated, userId]); // ⚡ 使用 userId 而不是 user?.id // 监听用户变化,重置检查标志(用户切换或退出登录时) React.useEffect(() => { - if (!isAuthenticated || !user) { - // 用户退出登录,重置标志 - hasCheckedCompleteness.current = false; - setProfileCompleteness(null); - setShowCompletenessAlert(false); + const userIdChanged = prevUserIdRef.current !== userId; + const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated; + + if (userIdChanged || authChanged) { + prevUserIdRef.current = userId; + prevIsAuthenticatedRef.current = isAuthenticated; + + if (!isAuthenticated || !user) { + // 用户退出登录,重置标志 + hasCheckedCompleteness.current = false; + setProfileCompleteness(null); + setShowCompletenessAlert(false); + } } - }, [isAuthenticated, user?.id]); // 监听用户 ID 变化 + }, [isAuthenticated, userId, user]); // ⚡ 使用 userId // 用户登录后检查资料完整性 React.useEffect(() => { - if (isAuthenticated && user) { + const userIdChanged = prevUserIdRef.current !== userId; + const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated; + + if ((userIdChanged || authChanged) && isAuthenticated && user) { // 延迟检查,避免过于频繁 const timer = setTimeout(checkProfileCompleteness, 1000); return () => clearTimeout(timer); } - }, [isAuthenticated, user?.id, checkProfileCompleteness]); // 只依赖 user.id,避免无限循环 + }, [isAuthenticated, userId, checkProfileCompleteness, user]); // ⚡ 使用 userId // 加载订阅信息 React.useEffect(() => { - if (isAuthenticated && user) { - const loadSubscriptionInfo = async () => { - try { - const base = getApiBase(); - const response = await fetch(base + '/api/subscription/current', { - credentials: 'include', - }); - if (response.ok) { - const data = await response.json(); - if (data.success && data.data) { - // 数据标准化处理:确保type字段是小写的 'free', 'pro', 或 'max' - 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 - }; - setSubscriptionInfo(normalizedData); + const userIdChanged = prevUserIdRef.current !== userId; + const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated; + + if (userIdChanged || authChanged) { + if (isAuthenticated && user) { + const loadSubscriptionInfo = async () => { + try { + const base = getApiBase(); + const response = await fetch(base + '/api/subscription/current', { + credentials: 'include', + }); + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + // 数据标准化处理:确保type字段是小写的 'free', 'pro', 或 'max' + 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 + }; + setSubscriptionInfo(normalizedData); + } } + } catch (error) { + logger.error('HomeNavbar', '加载订阅信息失败', error); } - } catch (error) { - logger.error('HomeNavbar', '加载订阅信息失败', error); - } - }; - loadSubscriptionInfo(); - } else { - // 用户未登录时,重置为免费版 - setSubscriptionInfo({ - type: 'free', - status: 'active', - days_left: 0, - is_active: true - }); + }; + loadSubscriptionInfo(); + } else { + // 用户未登录时,重置为免费版 + setSubscriptionInfo({ + type: 'free', + status: 'active', + days_left: 0, + is_active: true + }); + } } - }, [isAuthenticated, user?.id]); // 只依赖 user.id 而不是整个 user 对象 + }, [isAuthenticated, userId, user]); // ⚡ 使用 userId,防重复通过 ref 判断 return ( <> diff --git a/src/components/ProtectedRoute.js b/src/components/ProtectedRoute.js index 2c5206b8..a415a172 100755 --- a/src/components/ProtectedRoute.js +++ b/src/components/ProtectedRoute.js @@ -1,5 +1,5 @@ // src/components/ProtectedRoute.js - 弹窗拦截版本 -import React, { useEffect } from 'react'; +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'; @@ -8,15 +8,17 @@ const ProtectedRoute = ({ children }) => { const { isAuthenticated, isLoading, user } = useAuth(); const { openAuthModal, isAuthModalOpen } = useAuthModal(); - // 记录当前路径,登录成功后可以跳转回来 - const currentPath = window.location.pathname + window.location.search; + // ⚡ 使用 useRef 保存当前路径,避免每次渲染创建新字符串导致 useEffect 无限循环 + const currentPathRef = useRef(window.location.pathname + window.location.search); // 未登录时自动弹出认证窗口 useEffect(() => { if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) { - openAuthModal(currentPath); + openAuthModal(currentPathRef.current); } - }, [isAuthenticated, user, isLoading, isAuthModalOpen, currentPath, openAuthModal]); + // ⚠️ 移除 user 依赖,因为 user 对象每次从 API 返回都是新引用,会导致无限循环 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthenticated, isLoading, isAuthModalOpen, openAuthModal]); // 显示加载状态 if (isLoading) { diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js index add8e8bc..c4deae2b 100755 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -26,6 +26,9 @@ export const AuthProvider = ({ children }) => { const toast = useToast(); const { showWelcomeGuide } = useNotification(); + // ⚡ 使用 ref 保存最新的 isAuthenticated 值,避免事件监听器重复注册 + const isAuthenticatedRef = React.useRef(isAuthenticated); + // 检查Session状态 const checkSession = async () => { try { @@ -57,19 +60,27 @@ export const AuthProvider = ({ children }) => { }); if (data.isAuthenticated && data.user) { - setUser(data.user); - setIsAuthenticated(true); + // ⚡ 只在 user 数据真正变化时才更新状态,避免无限循环 + setUser((prevUser) => { + // 比较用户 ID,如果相同则不更新 + if (prevUser && prevUser.id === data.user.id) { + return prevUser; + } + return data.user; + }); + setIsAuthenticated((prev) => prev === true ? prev : true); } else { - setUser(null); - setIsAuthenticated(false); + setUser((prev) => prev === null ? prev : null); + setIsAuthenticated((prev) => prev === false ? prev : false); } } catch (error) { logger.error('AuthContext', 'checkSession', error); // 网络错误或超时,设置为未登录状态 - setUser(null); - setIsAuthenticated(false); + setUser((prev) => prev === null ? prev : null); + setIsAuthenticated((prev) => prev === false ? prev : false); } finally { - setIsLoading(false); + // ⚡ 只在 isLoading 为 true 时才设置为 false,避免不必要的状态更新 + setIsLoading((prev) => prev === false ? prev : false); } }; @@ -79,11 +90,17 @@ export const AuthProvider = ({ children }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // ⚡ 同步 isAuthenticated 到 ref + useEffect(() => { + isAuthenticatedRef.current = isAuthenticated; + }, [isAuthenticated]); + // 监听路由变化,检查session(处理微信登录回调) + // ⚡ 移除 isAuthenticated 依赖,使用 ref 避免重复注册事件监听器 useEffect(() => { const handleRouteChange = () => { - // 如果是从微信回调返回的,重新检查session - if (window.location.pathname === '/home' && !isAuthenticated) { + // 使用 ref 获取最新的认证状态 + if (window.location.pathname === '/home' && !isAuthenticatedRef.current) { checkSession(); } }; @@ -91,7 +108,7 @@ export const AuthProvider = ({ children }) => { window.addEventListener('popstate', handleRouteChange); return () => window.removeEventListener('popstate', handleRouteChange); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAuthenticated]); + }, []); // ✅ 空依赖数组,只注册一次事件监听器 // 更新本地用户的便捷方法 const updateUser = (partial) => {