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
+ };
+};