import React, { useCallback, useState } from 'react'; import { Box, Flex, Text, Button, Container, useDisclosure, HStack, Icon, Menu, MenuButton, MenuList, MenuItem, Badge, Grid, IconButton, useBreakpointValue, Spinner, useColorMode, useColorModeValue, useToast, } from '@chakra-ui/react'; import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi'; import { FaCrown } from 'react-icons/fa'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { useAuthModal } from '../../hooks/useAuthModal'; import { logger } from '../../utils/logger'; import { getApiBase } from '../../utils/apiConfig'; import SubscriptionButton from '../Subscription/SubscriptionButton'; import { useNavigationEvents } from '../../hooks/useNavigationEvents'; // Phase 1 优化: 提取的子组件 import BrandLogo from './components/BrandLogo'; import LoginButton from './components/LoginButton'; import CalendarButton from './components/CalendarButton'; // Phase 2 优化: 使用 Redux 管理订阅数据 import { useSubscription } from '../../hooks/useSubscription'; // Phase 3 优化: 提取的用户菜单组件 import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu'; // Phase 4 优化: 提取的导航菜单组件 import { DesktopNav, MoreMenu, PersonalCenterMenu } from './components/Navigation'; // Phase 5 优化: 提取的移动端抽屉菜单组件 import { MobileDrawer } from './components/MobileDrawer'; /** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ 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'); // 🎯 初始化导航埋点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 4: MoreNavMenu 和 NavItems 组件已提取到 Navigation 目录 export default function HomeNavbar() { const { isOpen, onOpen, onClose } = useDisclosure(); const navigate = useNavigate(); const isMobile = useBreakpointValue({ base: true, md: false }); const isTablet = useBreakpointValue({ base: false, md: true, lg: false }); const isDesktop = useBreakpointValue({ base: false, md: false, lg: true }); const { user, isAuthenticated, logout, isLoading } = useAuth(); const { openAuthModal } = useAuthModal(); const { colorMode, toggleColorMode } = useColorMode(); const navbarBg = useColorModeValue('white', 'gray.800'); const navbarBorder = useColorModeValue('gray.200', 'gray.700'); const brandText = useColorModeValue('gray.800', 'white'); const brandHover = useColorModeValue('blue.600', 'blue.300'); const toast = useToast(); // 🎯 初始化导航埋点Hook const navEvents = useNavigationEvents({ component: 'main_navbar' }); // ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环 const userId = user?.id; const prevUserIdRef = React.useRef(userId); const prevIsAuthenticatedRef = React.useRef(isAuthenticated); // 添加调试信息 - 暂时注释以减少日志噪音 // logger.debug('HomeNavbar', '组件渲染状态', { // hasUser: !!user, // isAuthenticated, // isLoading, // userId: user?.id // }); // 获取显示名称的函数 const getDisplayName = () => { if (!user) return ''; return user.nickname || user.username || user.name || user.email || '用户'; }; // 处理登出 const handleLogout = async () => { try { await logout(); // 重置资料完整性检查标志 hasCheckedCompleteness.current = false; setProfileCompleteness(null); setShowCompletenessAlert(false); // logout函数已经包含了跳转逻辑,这里不需要额外处理 } catch (error) { logger.error('HomeNavbar', 'handleLogout', error, { userId: user?.id }); } }; // 检查是否为禁用的链接(没有NEW标签的链接) // const isDisabledLink = true; // 自选股 / 关注事件 下拉所需状态 const [watchlistQuotes, setWatchlistQuotes] = useState([]); const [watchlistLoading, setWatchlistLoading] = useState(false); const [followingEvents, setFollowingEvents] = useState([]); const [eventsLoading, setEventsLoading] = useState(false); const [watchlistPage, setWatchlistPage] = useState(1); const [eventsPage, setEventsPage] = useState(1); const WATCHLIST_PAGE_SIZE = 10; const EVENTS_PAGE_SIZE = 8; // 投资日历 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, isSubscriptionModalOpen, openSubscriptionModal, closeSubscriptionModal } = useSubscription(); const loadWatchlistQuotes = useCallback(async () => { try { setWatchlistLoading(true); const base = getApiBase(); // 使用外部函数 const resp = await fetch(base + '/api/account/watchlist/realtime', { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }); if (resp.ok) { const data = await resp.json(); if (data && data.success && Array.isArray(data.data)) { setWatchlistQuotes(data.data); } else { setWatchlistQuotes([]); } } else { setWatchlistQuotes([]); } } catch (e) { logger.warn('HomeNavbar', '加载自选股实时行情失败', { error: e.message }); setWatchlistQuotes([]); } finally { setWatchlistLoading(false); } }, []); // getApiBase 是外部函数,不需要作为依赖 const loadFollowingEvents = useCallback(async () => { try { setEventsLoading(true); const base = getApiBase(); const resp = await fetch(base + '/api/account/events/following', { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }); if (resp.ok) { const data = await resp.json(); if (data && data.success && Array.isArray(data.data)) { const ids = data.data.map((e) => e.id).filter(Boolean); if (ids.length === 0) { setFollowingEvents([]); } else { // 并行请求详情以获取涨幅字段 const detailResponses = await Promise.all(ids.map((id) => fetch(base + `/api/events/${id}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }))); const detailJsons = await Promise.all(detailResponses.map((r) => r.ok ? r.json() : Promise.resolve({ success: false }))); const details = detailJsons .filter((j) => j && j.success && j.data) .map((j) => j.data); // 以原顺序合并,缺失则回退基础信息 const merged = ids.map((id) => { const d = details.find((x) => x.id === id); const baseItem = (data.data || []).find((x) => x.id === id) || {}; return d ? d : baseItem; }); setFollowingEvents(merged); } } else { setFollowingEvents([]); } } else { setFollowingEvents([]); } } catch (e) { logger.warn('HomeNavbar', '加载关注事件失败', { error: e.message }); setFollowingEvents([]); } finally { setEventsLoading(false); } }, []); // getApiBase 是外部函数,不需要作为依赖 // 从自选股移除 const handleRemoveFromWatchlist = useCallback(async (stockCode) => { try { const base = getApiBase(); const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, { method: 'DELETE', credentials: 'include' }); const data = await resp.json().catch(() => ({})); if (resp.ok && data && data.success !== false) { setWatchlistQuotes((prev) => { const normalize6 = (code) => { const m = String(code || '').match(/(\d{6})/); return m ? m[1] : String(code || ''); }; const target = normalize6(stockCode); const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target); const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE)); setWatchlistPage((p) => Math.min(p, newMaxPage)); return updated; }); toast({ title: '已从自选股移除', status: 'info', duration: 1500 }); } else { toast({ title: '移除失败', status: 'error', duration: 2000 }); } } catch (e) { toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 }); } }, [toast]); // WATCHLIST_PAGE_SIZE 是常量,getApiBase 是外部函数,不需要作为依赖 // 取消关注事件 const handleUnfollowEvent = useCallback(async (eventId) => { try { const base = getApiBase(); const resp = await fetch(base + `/api/events/${eventId}/follow`, { method: 'POST', credentials: 'include' }); const data = await resp.json().catch(() => ({})); if (resp.ok && data && data.success !== false) { setFollowingEvents((prev) => { const updated = (prev || []).filter((x) => x.id !== eventId); const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / EVENTS_PAGE_SIZE)); setEventsPage((p) => Math.min(p, newMaxPage)); return updated; }); toast({ title: '已取消关注该事件', status: 'info', duration: 1500 }); } else { toast({ title: '操作失败', status: 'error', duration: 2000 }); } } catch (e) { toast({ title: '网络错误,操作失败', status: 'error', duration: 2000 }); } }, [toast]); // EVENTS_PAGE_SIZE 是常量,getApiBase 是外部函数,不需要作为依赖 // 检查用户资料完整性 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' }} /> )} {/* Logo - 价小前投研 */} {/* 中间导航区域 - 响应式 (Phase 4 优化) */} {isMobile ? ( // 移动端:汉堡菜单 } variant="ghost" onClick={onOpen} aria-label="Open menu" /> ) : isTablet ? ( // 中屏(平板):"更多"下拉菜单 ) : ( // 大屏(桌面):完整导航菜单 )} {/* 右侧:日夜模式切换 + 登录/用户区 */} : } 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 && } {/* 自选股 - 仅大屏显示 */} {isDesktop && ( } leftIcon={} > 自选股 {watchlistQuotes && watchlistQuotes.length > 0 && ( {watchlistQuotes.length} )} 我的自选股 {watchlistLoading ? ( 加载中... ) : ( <> {(!watchlistQuotes || watchlistQuotes.length === 0) ? ( 暂无自选股 ) : ( {watchlistQuotes .slice((watchlistPage - 1) * WATCHLIST_PAGE_SIZE, watchlistPage * WATCHLIST_PAGE_SIZE) .map((item) => ( navigate(`/company?scode=${item.stock_code}`)}> {item.stock_name || item.stock_code} {item.stock_code} 0 ? 'red' : ((item.change_percent || 0) < 0 ? 'green' : 'gray')} fontSize="xs" > {(item.change_percent || 0) > 0 ? '+' : ''}{(item.change_percent || 0).toFixed(2)}% {item.current_price?.toFixed ? item.current_price.toFixed(2) : (item.current_price || '-')} ))} )} {watchlistPage} / {Math.max(1, Math.ceil((watchlistQuotes?.length || 0) / WATCHLIST_PAGE_SIZE))} )} )} {/* 关注的事件 - 仅大屏显示 */} {isDesktop && ( } leftIcon={} > 自选事件 {followingEvents && followingEvents.length > 0 && ( {followingEvents.length} )} 我关注的事件 {eventsLoading ? ( 加载中... ) : ( <> {(!followingEvents || followingEvents.length === 0) ? ( 暂未关注任何事件 ) : ( {followingEvents .slice((eventsPage - 1) * EVENTS_PAGE_SIZE, eventsPage * EVENTS_PAGE_SIZE) .map((ev) => ( navigate(`/event-detail/${ev.id}`)}> {ev.title} {ev.event_type && ( {ev.event_type} )} {ev.start_time && ( {new Date(ev.start_time).toLocaleString('zh-CN')} )} {typeof ev.related_avg_chg === 'number' && ( 0 ? 'red' : (ev.related_avg_chg < 0 ? 'green' : 'gray')} fontSize="xs">日均 {ev.related_avg_chg > 0 ? '+' : ''}{ev.related_avg_chg.toFixed(2)}% )} {typeof ev.related_week_chg === 'number' && ( 0 ? 'red' : (ev.related_week_chg < 0 ? 'green' : 'gray')} fontSize="xs">周涨 {ev.related_week_chg > 0 ? '+' : ''}{ev.related_week_chg.toFixed(2)}% )} ))} )} {eventsPage} / {Math.max(1, Math.ceil((followingEvents?.length || 0) / EVENTS_PAGE_SIZE))} )} )} {/* 头像区域 - 响应式 (Phase 3 优化) */} {isDesktop ? ( ) : ( )} {/* 个人中心下拉菜单 - 仅大屏显示 (Phase 4 优化) */} {isDesktop && ( )} ) : ( // 未登录状态 - 单一按钮 )} {/* 移动端抽屉菜单 (Phase 5 优化) */} {/* 二级导航栏 - 显示当前页面所属的二级菜单 */} {!isMobile && } {/* 投资日历 Modal - 已移至 CalendarButton 组件内部 */} ); }