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 && (
)}
{/* 关注的事件 - 仅大屏显示 */}
{isDesktop && (
)}
{/* 头像区域 - 响应式 (Phase 3 优化) */}
{isDesktop ? (
) : (
)}
{/* 个人中心下拉菜单 - 仅大屏显示 (Phase 4 优化) */}
{isDesktop && (
)}
) : (
// 未登录状态 - 单一按钮
)}
{/* 移动端抽屉菜单 (Phase 5 优化) */}
{/* 二级导航栏 - 显示当前页面所属的二级菜单 */}
{!isMobile && }
{/* 投资日历 Modal - 已移至 CalendarButton 组件内部 */}
>
);
}