diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index 83c24b24..4af73df8 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -54,157 +54,13 @@ import { WatchlistMenu, FollowingEventsMenu } from './components/FeatureMenus'; import { useWatchlist } from '../../hooks/useWatchlist'; import { useFollowingEvents } from '../../hooks/useFollowingEvents'; -/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ -const SecondaryNav = ({ showCompletenessAlert }) => { - const navigate = useNavigate(); - const location = useLocation(); - const navbarBg = useColorModeValue('gray.50', 'gray.700'); - const itemHoverBg = useColorModeValue('white', 'gray.600'); - // ⚠️ 必须在组件顶层调用所有Hooks(不能在JSX中调用) - const borderColorValue = useColorModeValue('gray.200', 'gray.600'); +// Phase 7 优化: 提取的二级导航、资料完整性、右侧功能区组件 +import SecondaryNav from './components/SecondaryNav'; +import ProfileCompletenessAlert from './components/ProfileCompletenessAlert'; +import { useProfileCompleteness } from '../../hooks/useProfileCompleteness'; +import NavbarActions from './components/NavbarActions'; - // 🎯 初始化导航埋点Hook - const navEvents = useNavigationEvents({ component: 'secondary_nav' }); - - // 定义二级导航结构 - const secondaryNavConfig = { - '/community': { - title: '高频跟踪', - items: [ - { path: '/community', label: '事件中心', badges: [{ text: 'HOT', colorScheme: 'green' }, { text: 'NEW', colorScheme: 'red' }] }, - { path: '/concepts', label: '概念中心', badges: [{ text: 'NEW', colorScheme: 'red' }] } - ] - }, - '/concepts': { - title: '高频跟踪', - items: [ - { path: '/community', label: '事件中心', badges: [{ text: 'HOT', colorScheme: 'green' }, { text: 'NEW', colorScheme: 'red' }] }, - { path: '/concepts', label: '概念中心', badges: [{ text: 'NEW', colorScheme: 'red' }] } - ] - }, - '/limit-analyse': { - title: '行情复盘', - items: [ - { path: '/limit-analyse', label: '涨停分析', badges: [{ text: 'FREE', colorScheme: 'blue' }] }, - { path: '/stocks', label: '个股中心', badges: [{ text: 'HOT', colorScheme: 'green' }] }, - { path: '/trading-simulation', label: '模拟盘', badges: [{ text: 'NEW', colorScheme: 'red' }] } - ] - }, - '/stocks': { - title: '行情复盘', - items: [ - { path: '/limit-analyse', label: '涨停分析', badges: [{ text: 'FREE', colorScheme: 'blue' }] }, - { path: '/stocks', label: '个股中心', badges: [{ text: 'HOT', colorScheme: 'green' }] }, - { path: '/trading-simulation', label: '模拟盘', badges: [{ text: 'NEW', colorScheme: 'red' }] } - ] - }, - '/trading-simulation': { - title: '行情复盘', - items: [ - { path: '/limit-analyse', label: '涨停分析', badges: [{ text: 'FREE', colorScheme: 'blue' }] }, - { path: '/stocks', label: '个股中心', badges: [{ text: 'HOT', colorScheme: 'green' }] }, - { path: '/trading-simulation', label: '模拟盘', badges: [{ text: 'NEW', colorScheme: 'red' }] } - ] - } - }; - - // 找到当前路径对应的二级导航配置 - const currentConfig = Object.keys(secondaryNavConfig).find(key => - location.pathname.includes(key) - ); - - // 如果没有匹配的二级导航,不显示 - if (!currentConfig) return null; - - const config = secondaryNavConfig[currentConfig]; - - return ( - - - - {/* 显示一级菜单标题 */} - - {config.title}: - - {/* 二级菜单项 */} - {config.items.map((item, index) => { - const isActive = location.pathname.includes(item.path); - return item.external ? ( - - ) : ( - - ); - })} - - - - ); -}; - -/** 中屏"更多"菜单 - 用于平板和小笔记本 */ +// Phase 7: SecondaryNav 组件已提取到 ./components/SecondaryNav/index.js // Phase 4: MoreNavMenu 和 NavItems 组件已提取到 Navigation 目录 export default function HomeNavbar() { @@ -244,14 +100,25 @@ export default function HomeNavbar() { return user.nickname || user.username || user.name || user.email || '用户'; }; + // Phase 6: 自选股和关注事件逻辑已提取到自定义 Hooks + const { watchlistQuotes, followingEvents } = useWatchlist(); + const { followingEvents: events } = useFollowingEvents(); + // 注意:这里只需要数据用于 TabletUserMenu,实际的菜单组件会自己管理状态 + + // Phase 7: 资料完整性逻辑已提取到 useProfileCompleteness Hook + const { + profileCompleteness, + showAlert: showCompletenessAlert, + setShowAlert: setShowCompletenessAlert, + resetCompleteness + } = useProfileCompleteness({ isAuthenticated, user }); + // 处理登出 const handleLogout = async () => { try { await logout(); - // 重置资料完整性检查标志 - hasCheckedCompleteness.current = false; - setProfileCompleteness(null); - setShowCompletenessAlert(false); + // Phase 7: 使用 resetCompleteness 重置资料完整性状态 + resetCompleteness(); // logout函数已经包含了跳转逻辑,这里不需要额外处理 } catch (error) { logger.error('HomeNavbar', 'handleLogout', error, { @@ -260,22 +127,6 @@ export default function HomeNavbar() { } }; - - // Phase 6: 自选股和关注事件逻辑已提取到自定义 Hooks - const { watchlistQuotes, followingEvents } = useWatchlist(); - const { followingEvents: events } = useFollowingEvents(); - // 注意:这里只需要数据用于 TabletUserMenu,实际的菜单组件会自己管理状态 - - // 投资日历 Modal 状态 - 已移至 CalendarButton 组件内部管理 - // const [calendarModalOpen, setCalendarModalOpen] = useState(false); - - // 用户信息完整性状态 - const [profileCompleteness, setProfileCompleteness] = useState(null); - const [showCompletenessAlert, setShowCompletenessAlert] = useState(false); - - // 添加标志位:追踪是否已经检查过资料完整性(避免重复请求) - const hasCheckedCompleteness = React.useRef(false); - // Phase 2: 使用 Redux 订阅数据 const { subscriptionInfo, @@ -287,133 +138,17 @@ export default function HomeNavbar() { // Phase 6: loadWatchlistQuotes, loadFollowingEvents, handleRemoveFromWatchlist, // handleUnfollowEvent 已移至自定义 Hooks 中,由各自组件内部管理 - // 检查用户资料完整性 - const checkProfileCompleteness = useCallback(async () => { - if (!isAuthenticated || !user) return; - - // 如果已经检查过,跳过(避免重复请求) - if (hasCheckedCompleteness.current) { - logger.debug('HomeNavbar', '已检查过资料完整性,跳过重复请求', { - userId: user?.id - }); - return; - } - - try { - logger.debug('HomeNavbar', '开始检查资料完整性', { - userId: user?.id - }); - const base = getApiBase(); - const resp = await fetch(base + '/api/account/profile-completeness', { - credentials: 'include' - }); - - if (resp.ok) { - const data = await resp.json(); - if (data.success) { - setProfileCompleteness(data.data); - // 只有微信用户且资料不完整时才显示提醒 - setShowCompletenessAlert(data.data.needsAttention); - // 标记为已检查 - hasCheckedCompleteness.current = true; - logger.debug('HomeNavbar', '资料完整性检查完成', { - userId: user?.id, - completeness: data.data.completenessPercentage - }); - } - } - } catch (error) { - logger.warn('HomeNavbar', '检查资料完整性失败', { - userId: user?.id, - error: error.message - }); - } - }, [isAuthenticated, userId]); // ⚡ 使用 userId 而不是 user?.id - - // 监听用户变化,重置检查标志(用户切换或退出登录时) - React.useEffect(() => { - 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, userId, user]); // ⚡ 使用 userId - - // 用户登录后检查资料完整性 - React.useEffect(() => { - const userIdChanged = prevUserIdRef.current !== userId; - const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated; - - if ((userIdChanged || authChanged) && isAuthenticated && user) { - // 延迟检查,避免过于频繁 - const timer = setTimeout(checkProfileCompleteness, 1000); - return () => clearTimeout(timer); - } - }, [isAuthenticated, userId, checkProfileCompleteness, user]); // ⚡ 使用 userId - // Phase 2: 加载订阅信息逻辑已移至 useSubscriptionData Hook return ( <> - {/* 资料完整性提醒横幅 */} - {showCompletenessAlert && profileCompleteness && ( - - - - - - - - 完善资料,享受更好服务 - - - 您还需要设置:{profileCompleteness.missingItems.join('、')} - - - - {profileCompleteness.completenessPercentage}% 完成 - - - - - ×} - onClick={() => setShowCompletenessAlert(false)} - aria-label="关闭提醒" - minW={{ base: '32px', md: '40px' }} - /> - - - - + {/* 资料完整性提醒横幅 (Phase 7 优化) */} + {showCompletenessAlert && ( + setShowCompletenessAlert(false)} + onNavigateToSettings={() => navigate('/home/settings')} + /> )} )} - {/* 右侧:日夜模式切换 + 登录/用户区 */} - - : } - onClick={() => { - // 🎯 追踪主题切换 - const fromTheme = colorMode; - const toTheme = colorMode === 'light' ? 'dark' : 'light'; - navEvents.trackThemeChanged(fromTheme, toTheme); - toggleColorMode(); - }} - variant="ghost" - size="sm" - minW={{ base: '36px', md: '40px' }} - minH={{ base: '36px', md: '40px' }} - /> - - {/* 显示加载状态 */} - {isLoading ? ( - - ) : isAuthenticated && user ? ( - // 已登录状态 - 用户菜单 + 功能菜单排列 - - {/* 投资日历 - 仅大屏显示 */} - {isDesktop && } - - {/* 自选股 - 仅大屏显示 (Phase 6 优化) */} - {isDesktop && } - - {/* 关注的事件 - 仅大屏显示 (Phase 6 优化) */} - {isDesktop && } - - {/* 头像区域 - 响应式 (Phase 3 优化) */} - {isDesktop ? ( - - ) : ( - - )} - - {/* 个人中心下拉菜单 - 仅大屏显示 (Phase 4 优化) */} - {isDesktop && ( - - )} - - ) : ( - // 未登录状态 - 单一按钮 - - )} - + {/* 右侧功能区 (Phase 7 优化) */} + diff --git a/src/components/Navbars/components/NavbarActions/index.js b/src/components/Navbars/components/NavbarActions/index.js new file mode 100644 index 00000000..8610708f --- /dev/null +++ b/src/components/Navbars/components/NavbarActions/index.js @@ -0,0 +1,82 @@ +// src/components/Navbars/components/NavbarActions/index.js +// Navbar 右侧功能区组件 + +import React, { memo } from 'react'; +import { HStack, Spinner } from '@chakra-ui/react'; +import ThemeToggleButton from '../ThemeToggleButton'; +import LoginButton from '../LoginButton'; +import CalendarButton from '../CalendarButton'; +import { WatchlistMenu, FollowingEventsMenu } from '../FeatureMenus'; +import { DesktopUserMenu, TabletUserMenu } from '../UserMenu'; +import { PersonalCenterMenu } from '../Navigation'; + +/** + * Navbar 右侧功能区组件 + * 根据用户登录状态和屏幕尺寸显示不同的操作按钮和菜单 + * + * @param {Object} props + * @param {boolean} props.isLoading - 是否正在加载 + * @param {boolean} props.isAuthenticated - 是否已登录 + * @param {Object} props.user - 用户对象 + * @param {boolean} props.isDesktop - 是否为桌面端 + * @param {Function} props.handleLogout - 登出回调 + * @param {Array} props.watchlistQuotes - 自选股数据(用于 TabletUserMenu) + * @param {Array} props.followingEvents - 关注事件数据(用于 TabletUserMenu) + */ +const NavbarActions = memo(({ + isLoading, + isAuthenticated, + user, + isDesktop, + handleLogout, + watchlistQuotes, + followingEvents +}) => { + return ( + + {/* 主题切换按钮 */} + + + {/* 显示加载状态 */} + {isLoading ? ( + + ) : isAuthenticated && user ? ( + // 已登录状态 - 用户菜单 + 功能菜单排列 + + {/* 投资日历 - 仅大屏显示 */} + {isDesktop && } + + {/* 自选股 - 仅大屏显示 */} + {isDesktop && } + + {/* 关注的事件 - 仅大屏显示 */} + {isDesktop && } + + {/* 头像区域 - 响应式 */} + {isDesktop ? ( + + ) : ( + + )} + + {/* 个人中心下拉菜单 - 仅大屏显示 */} + {isDesktop && ( + + )} + + ) : ( + // 未登录状态 - 单一按钮 + + )} + + ); +}); + +NavbarActions.displayName = 'NavbarActions'; + +export default NavbarActions; diff --git a/src/components/Navbars/components/ProfileCompletenessAlert/index.js b/src/components/Navbars/components/ProfileCompletenessAlert/index.js new file mode 100644 index 00000000..cc5119d5 --- /dev/null +++ b/src/components/Navbars/components/ProfileCompletenessAlert/index.js @@ -0,0 +1,96 @@ +// src/components/Navbars/components/ProfileCompletenessAlert/index.js +// 用户资料完整性提醒横幅组件 + +import React, { memo } from 'react'; +import { + Box, + Container, + HStack, + VStack, + Text, + Button, + IconButton, + Icon +} from '@chakra-ui/react'; +import { FiStar } from 'react-icons/fi'; + +/** + * 资料完整性提醒横幅组件 + * 显示用户资料完整度和缺失项提示 + * + * @param {Object} props + * @param {Object} props.profileCompleteness - 资料完整度数据 + * @param {Array} props.profileCompleteness.missingItems - 缺失的项目列表 + * @param {number} props.profileCompleteness.completenessPercentage - 完成百分比 + * @param {Function} props.onClose - 关闭横幅回调 + * @param {Function} props.onNavigateToSettings - 导航到设置页面回调 + */ +const ProfileCompletenessAlert = memo(({ + profileCompleteness, + onClose, + onNavigateToSettings +}) => { + if (!profileCompleteness) return null; + + return ( + + + + + + + + 完善资料,享受更好服务 + + + 您还需要设置:{profileCompleteness.missingItems.join('、')} + + + + {profileCompleteness.completenessPercentage}% 完成 + + + + + ×} + onClick={onClose} + aria-label="关闭提醒" + minW={{ base: '32px', md: '40px' }} + /> + + + + + ); +}); + +ProfileCompletenessAlert.displayName = 'ProfileCompletenessAlert'; + +export default ProfileCompletenessAlert; diff --git a/src/components/Navbars/components/SecondaryNav/config.js b/src/components/Navbars/components/SecondaryNav/config.js new file mode 100644 index 00000000..436d8623 --- /dev/null +++ b/src/components/Navbars/components/SecondaryNav/config.js @@ -0,0 +1,111 @@ +// src/components/Navbars/components/SecondaryNav/config.js +// 二级导航配置数据 + +/** + * 二级导航配置结构 + * - key: 匹配的路径前缀 + * - title: 导航组标题 + * - items: 导航项列表 + * - path: 路径 + * - label: 显示文本 + * - badges: 徽章列表 (可选) + * - external: 是否外部链接 (可选) + */ +export const secondaryNavConfig = { + '/community': { + title: '高频跟踪', + items: [ + { + path: '/community', + label: '事件中心', + badges: [ + { text: 'HOT', colorScheme: 'green' }, + { text: 'NEW', colorScheme: 'red' } + ] + }, + { + path: '/concepts', + label: '概念中心', + badges: [{ text: 'NEW', colorScheme: 'red' }] + } + ] + }, + '/concepts': { + title: '高频跟踪', + items: [ + { + path: '/community', + label: '事件中心', + badges: [ + { text: 'HOT', colorScheme: 'green' }, + { text: 'NEW', colorScheme: 'red' } + ] + }, + { + path: '/concepts', + label: '概念中心', + badges: [{ text: 'NEW', colorScheme: 'red' }] + } + ] + }, + '/limit-analyse': { + title: '行情复盘', + items: [ + { + path: '/limit-analyse', + label: '涨停分析', + badges: [{ text: 'FREE', colorScheme: 'blue' }] + }, + { + path: '/stocks', + label: '个股中心', + badges: [{ text: 'HOT', colorScheme: 'green' }] + }, + { + path: '/trading-simulation', + label: '模拟盘', + badges: [{ text: 'NEW', colorScheme: 'red' }] + } + ] + }, + '/stocks': { + title: '行情复盘', + items: [ + { + path: '/limit-analyse', + label: '涨停分析', + badges: [{ text: 'FREE', colorScheme: 'blue' }] + }, + { + path: '/stocks', + label: '个股中心', + badges: [{ text: 'HOT', colorScheme: 'green' }] + }, + { + path: '/trading-simulation', + label: '模拟盘', + badges: [{ text: 'NEW', colorScheme: 'red' }] + } + ] + }, + '/trading-simulation': { + title: '行情复盘', + items: [ + { + path: '/limit-analyse', + label: '涨停分析', + badges: [{ text: 'FREE', colorScheme: 'blue' }] + }, + { + path: '/stocks', + label: '个股中心', + badges: [{ text: 'HOT', colorScheme: 'green' }] + }, + { + path: '/trading-simulation', + label: '模拟盘', + badges: [{ text: 'NEW', colorScheme: 'red' }] + } + ] + } +}; diff --git a/src/components/Navbars/components/SecondaryNav/index.js b/src/components/Navbars/components/SecondaryNav/index.js new file mode 100644 index 00000000..e297a7fd --- /dev/null +++ b/src/components/Navbars/components/SecondaryNav/index.js @@ -0,0 +1,138 @@ +// src/components/Navbars/components/SecondaryNav/index.js +// 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 + +import React, { memo } from 'react'; +import { + Box, + Container, + HStack, + Text, + Button, + Flex, + Badge, + useColorModeValue +} from '@chakra-ui/react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useNavigationEvents } from '../../../../hooks/useNavigationEvents'; +import { secondaryNavConfig } from './config'; + +/** + * 二级导航栏组件 + * 根据当前路径显示对应的二级菜单项 + * + * @param {Object} props + * @param {boolean} props.showCompletenessAlert - 是否显示完整性提醒(影响 sticky top 位置) + */ +const SecondaryNav = memo(({ showCompletenessAlert }) => { + const navigate = useNavigate(); + const location = useLocation(); + + // 颜色模式 + const navbarBg = useColorModeValue('gray.50', 'gray.700'); + const itemHoverBg = useColorModeValue('white', 'gray.600'); + const borderColorValue = useColorModeValue('gray.200', 'gray.600'); + + // 导航埋点 + const navEvents = useNavigationEvents({ component: 'secondary_nav' }); + + // 找到当前路径对应的二级导航配置 + const currentConfig = Object.keys(secondaryNavConfig).find(key => + location.pathname.includes(key) + ); + + // 如果没有匹配的二级导航,不显示 + if (!currentConfig) return null; + + const config = secondaryNavConfig[currentConfig]; + + return ( + + + + {/* 显示一级菜单标题 */} + + {config.title}: + + + {/* 二级菜单项 */} + {config.items.map((item, index) => { + const isActive = location.pathname.includes(item.path); + + return item.external ? ( + + ) : ( + + ); + })} + + + + ); +}); + +SecondaryNav.displayName = 'SecondaryNav'; + +export default SecondaryNav; diff --git a/src/components/Navbars/components/ThemeToggleButton.js b/src/components/Navbars/components/ThemeToggleButton.js index 57e0b3d1..16e61580 100644 --- a/src/components/Navbars/components/ThemeToggleButton.js +++ b/src/components/Navbars/components/ThemeToggleButton.js @@ -1,30 +1,48 @@ // src/components/Navbars/components/ThemeToggleButton.js +// 主题切换按钮组件 - Phase 7 优化:添加导航埋点支持 + import React, { memo } from 'react'; -import { IconButton, useColorMode, Tooltip } from '@chakra-ui/react'; +import { IconButton, useColorMode } from '@chakra-ui/react'; import { SunIcon, MoonIcon } from '@chakra-ui/icons'; +import { useNavigationEvents } from '../../../hooks/useNavigationEvents'; /** * 主题切换按钮组件 + * 支持在亮色和暗色主题之间切换,包含导航埋点 * * 性能优化: * - 使用 memo 避免父组件重新渲染时的不必要更新 * - 只依赖 colorMode,当主题切换时才重新渲染 * + * @param {Object} props + * @param {string} props.size - 按钮大小,默认 'sm' + * @param {string} props.variant - 按钮样式,默认 'ghost' * @returns {JSX.Element} */ -const ThemeToggleButton = memo(() => { +const ThemeToggleButton = memo(({ size = 'sm', variant = 'ghost' }) => { const { colorMode, toggleColorMode } = useColorMode(); + const navEvents = useNavigationEvents({ component: 'theme_toggle' }); + + const handleToggle = () => { + // 追踪主题切换 + const fromTheme = colorMode; + const toTheme = colorMode === 'light' ? 'dark' : 'light'; + navEvents.trackThemeChanged(fromTheme, toTheme); + + // 切换主题 + toggleColorMode(); + }; return ( - - : } - onClick={toggleColorMode} - variant="ghost" - size="md" - /> - + : } + onClick={handleToggle} + variant={variant} + size={size} + minW={{ base: '36px', md: '40px' }} + minH={{ base: '36px', md: '40px' }} + /> ); }); diff --git a/src/hooks/useProfileCompleteness.js b/src/hooks/useProfileCompleteness.js new file mode 100644 index 00000000..0a2861cb --- /dev/null +++ b/src/hooks/useProfileCompleteness.js @@ -0,0 +1,127 @@ +// src/hooks/useProfileCompleteness.js +// 用户资料完整性管理自定义 Hook + +import { useState, useCallback, useRef, useEffect } from 'react'; +import { logger } from '../utils/logger'; +import { getApiBase } from '../utils/apiConfig'; + +/** + * 用户资料完整性管理 Hook + * 检查并管理用户资料完整度状态 + * + * @param {Object} options + * @param {boolean} options.isAuthenticated - 是否已登录 + * @param {Object} options.user - 用户对象 + * @returns {{ + * profileCompleteness: Object|null, + * showAlert: boolean, + * setShowAlert: Function, + * isChecking: boolean, + * checkProfileCompleteness: Function + * }} + */ +export const useProfileCompleteness = ({ isAuthenticated, user }) => { + const [profileCompleteness, setProfileCompleteness] = useState(null); + const [showAlert, setShowAlert] = useState(false); + const [isChecking, setIsChecking] = useState(false); + + // 添加标志位:追踪是否已经检查过资料完整性(避免重复请求) + const hasCheckedCompleteness = useRef(false); + + // ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环 + const userId = user?.id; + const prevUserIdRef = useRef(userId); + const prevIsAuthenticatedRef = useRef(isAuthenticated); + + // 检查用户资料完整性 + const checkProfileCompleteness = useCallback(async () => { + if (!isAuthenticated || !user) return; + + // 如果已经检查过,跳过(避免重复请求) + if (hasCheckedCompleteness.current) { + logger.debug('useProfileCompleteness', '已检查过资料完整性,跳过重复请求', { + userId: user?.id + }); + return; + } + + try { + setIsChecking(true); + logger.debug('useProfileCompleteness', '开始检查资料完整性', { + userId: user?.id + }); + const base = getApiBase(); + const resp = await fetch(base + '/api/account/profile-completeness', { + credentials: 'include' + }); + + if (resp.ok) { + const data = await resp.json(); + if (data.success) { + setProfileCompleteness(data.data); + // 只有微信用户且资料不完整时才显示提醒 + setShowAlert(data.data.needsAttention); + // 标记为已检查 + hasCheckedCompleteness.current = true; + logger.debug('useProfileCompleteness', '资料完整性检查完成', { + userId: user?.id, + completeness: data.data.completenessPercentage + }); + } + } + } catch (error) { + logger.warn('useProfileCompleteness', '检查资料完整性失败', { + userId: user?.id, + error: error.message + }); + } finally { + setIsChecking(false); + } + }, [isAuthenticated, userId, user]); + + // 监听用户变化,重置检查标志(用户切换或退出登录时) + useEffect(() => { + 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); + setShowAlert(false); + } + } + }, [isAuthenticated, userId, user]); + + // 用户登录后检查资料完整性 + useEffect(() => { + const userIdChanged = prevUserIdRef.current !== userId; + const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated; + + if ((userIdChanged || authChanged) && isAuthenticated && user) { + // 延迟检查,避免过于频繁 + const timer = setTimeout(checkProfileCompleteness, 1000); + return () => clearTimeout(timer); + } + }, [isAuthenticated, userId, checkProfileCompleteness, user]); + + // 提供重置函数,用于登出时清理 + const resetCompleteness = useCallback(() => { + hasCheckedCompleteness.current = false; + setProfileCompleteness(null); + setShowAlert(false); + }, []); + + return { + profileCompleteness, + showAlert, + setShowAlert, + isChecking, + checkProfileCompleteness, + resetCompleteness + }; +};