refactor(HomeNavbar): Phase 4 - 提取导航菜单组件
**背景** 继 Phase 1-3 后,进一步优化 HomeNavbar 的导航菜单结构 **重构内容** 1. **新增组件目录** `src/components/Navbars/components/Navigation/` - DesktopNav.js (200行) - 桌面版完整导航菜单(高频跟踪、行情复盘、AGENT社群、联系我们) - MoreMenu.js (135行) - 平板版"更多"下拉菜单(折叠所有导航项) - PersonalCenterMenu.js (102行) - 个人中心下拉菜单(用户信息、账户管理、订阅管理、退出登录) - index.js - 统一导出 2. **HomeNavbar.js 优化** - 删除 MoreNavMenu 组件定义 (~103行) - 删除 NavItems 组件定义 (~184行) - 删除 PersonalCenterMenu JSX (~40行) - 替换为组件调用 - 1394 → 1065 行 (-329行, -24%) **技术亮点** - React.memo 优化渲染性能 - useCallback 缓存导航激活状态判断 - 集成 useNavigationEvents 埋点追踪 - 响应式设计 (Desktop / Tablet / Mobile) - 组件内聚,降低主文件复杂度 **累计成果** (Phase 1-4) - 原始: 1623 行 - 当前: 1065 行 - 减少: 558 行 (-34%) - 提取: 10 个组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,9 @@ import { useSubscription } from '../../hooks/useSubscription';
|
|||||||
// Phase 3 优化: 提取的用户菜单组件
|
// Phase 3 优化: 提取的用户菜单组件
|
||||||
import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu';
|
import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu';
|
||||||
|
|
||||||
|
// Phase 4 优化: 提取的导航菜单组件
|
||||||
|
import { DesktopNav, MoreMenu, PersonalCenterMenu } from './components/Navigation';
|
||||||
|
|
||||||
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
||||||
const SecondaryNav = ({ showCompletenessAlert }) => {
|
const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -211,294 +214,7 @@ const SecondaryNav = ({ showCompletenessAlert }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 中屏"更多"菜单 - 用于平板和小笔记本 */
|
/** 中屏"更多"菜单 - 用于平板和小笔记本 */
|
||||||
const MoreNavMenu = ({ isAuthenticated, user }) => {
|
// Phase 4: MoreNavMenu 和 NavItems 组件已提取到 Navigation 目录
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
// 辅助函数:判断导航项是否激活
|
|
||||||
const isActive = useCallback((paths) => {
|
|
||||||
return paths.some(path => location.pathname.includes(path));
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
if (!isAuthenticated || !user) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
variant="ghost"
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
fontWeight="medium"
|
|
||||||
>
|
|
||||||
更多
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList minW="300px" p={2}>
|
|
||||||
{/* 高频跟踪组 */}
|
|
||||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/community')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">事件中心</Text>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/concepts')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">概念中心</Text>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
|
|
||||||
{/* 行情复盘组 */}
|
|
||||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/limit-analyse')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">涨停分析</Text>
|
|
||||||
<Badge size="sm" colorScheme="blue">FREE</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/stocks')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">个股中心</Text>
|
|
||||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/trading-simulation')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">模拟盘</Text>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
|
|
||||||
{/* AGENT社群组 */}
|
|
||||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">AGENT社群</Text>
|
|
||||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
|
||||||
<Text fontSize="sm" color="gray.400">今日热议</Text>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
|
||||||
<Text fontSize="sm" color="gray.400">个股社区</Text>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
|
|
||||||
{/* 联系我们 */}
|
|
||||||
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
|
||||||
<Text fontSize="sm" color="gray.400">联系我们</Text>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 桌面端导航 - 完全按照原网站
|
|
||||||
* @TODO 添加逻辑 不展示导航case
|
|
||||||
* 1.未登陆状态 && 是首页
|
|
||||||
* 2. !isMobile
|
|
||||||
*/
|
|
||||||
const NavItems = ({ isAuthenticated, user }) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
// ⚠️ 必须在组件顶层调用所有Hooks(不能在JSX中调用)
|
|
||||||
const contactTextColor = useColorModeValue('gray.500', 'gray.300');
|
|
||||||
|
|
||||||
// 🎯 初始化导航埋点Hook
|
|
||||||
const navEvents = useNavigationEvents({ component: 'top_nav' });
|
|
||||||
|
|
||||||
// 辅助函数:判断导航项是否激活
|
|
||||||
const isActive = useCallback((paths) => {
|
|
||||||
return paths.some(path => location.pathname.includes(path));
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
if (isAuthenticated && user) {
|
|
||||||
return (
|
|
||||||
<HStack spacing={8}>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
variant="ghost"
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
bg={isActive(['/community', '/concepts']) ? 'blue.50' : 'transparent'}
|
|
||||||
color={isActive(['/community', '/concepts']) ? 'blue.600' : 'inherit'}
|
|
||||||
fontWeight={isActive(['/community', '/concepts']) ? 'bold' : 'normal'}
|
|
||||||
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
|
|
||||||
>
|
|
||||||
高频跟踪
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList minW="260px" p={2}>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
// 🎯 追踪菜单项点击
|
|
||||||
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
|
|
||||||
navigate('/community');
|
|
||||||
}}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
|
||||||
borderLeft={location.pathname.includes('/community') ? '3px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
fontWeight={location.pathname.includes('/community') ? 'bold' : 'normal'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">事件中心</Text>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
// 🎯 追踪菜单项点击
|
|
||||||
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
|
|
||||||
navigate('/concepts');
|
|
||||||
}}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
|
||||||
borderLeft={location.pathname.includes('/concepts') ? '3px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
fontWeight={location.pathname.includes('/concepts') ? 'bold' : 'normal'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">概念中心</Text>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
variant="ghost"
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
bg={isActive(['/limit-analyse', '/stocks']) ? 'blue.50' : 'transparent'}
|
|
||||||
color={isActive(['/limit-analyse', '/stocks']) ? 'blue.600' : 'inherit'}
|
|
||||||
fontWeight={isActive(['/limit-analyse', '/stocks']) ? 'bold' : 'normal'}
|
|
||||||
borderBottom={isActive(['/limit-analyse', '/stocks']) ? '2px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
_hover={{ bg: isActive(['/limit-analyse', '/stocks']) ? 'blue.100' : 'gray.50' }}
|
|
||||||
>
|
|
||||||
行情复盘
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList minW="260px" p={2}>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/limit-analyse')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
|
||||||
borderLeft={location.pathname.includes('/limit-analyse') ? '3px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
fontWeight={location.pathname.includes('/limit-analyse') ? 'bold' : 'normal'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">涨停分析</Text>
|
|
||||||
<Badge size="sm" colorScheme="blue">FREE</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/stocks')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
|
||||||
borderLeft={location.pathname.includes('/stocks') ? '3px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
fontWeight={location.pathname.includes('/stocks') ? 'bold' : 'normal'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">个股中心</Text>
|
|
||||||
<Badge size="sm" colorScheme="green">HOT</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate('/trading-simulation')}
|
|
||||||
borderRadius="md"
|
|
||||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
|
||||||
borderLeft={location.pathname.includes('/trading-simulation') ? '3px solid' : 'none'}
|
|
||||||
borderColor="blue.600"
|
|
||||||
fontWeight={location.pathname.includes('/trading-simulation') ? 'bold' : 'normal'}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="center" w="100%">
|
|
||||||
<Text fontSize="sm">模拟盘</Text>
|
|
||||||
<Badge size="sm" colorScheme="red">NEW</Badge>
|
|
||||||
</Flex>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
{false && isAuthenticated && (
|
|
||||||
<Menu onOpen={loadWatchlistQuotes} closeOnSelect={false}>
|
|
||||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>自选股</MenuButton>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
|
||||||
AGENT社群
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList minW="300px" p={4}>
|
|
||||||
<MenuItem
|
|
||||||
isDisabled
|
|
||||||
cursor="not-allowed"
|
|
||||||
color="gray.400"
|
|
||||||
>
|
|
||||||
<Text fontSize="sm" color="gray.400">今日热议</Text>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
isDisabled
|
|
||||||
cursor="not-allowed"
|
|
||||||
color="gray.400"
|
|
||||||
>
|
|
||||||
<Text fontSize="sm" color="gray.400">个股社区</Text>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
{false && isAuthenticated && (
|
|
||||||
<Menu onOpen={loadFollowingEvents} closeOnSelect={false}>
|
|
||||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>关注的事件</MenuButton>
|
|
||||||
</Menu>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
|
||||||
联系我们
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList minW="260px" p={4}>
|
|
||||||
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</HStack>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function HomeNavbar() {
|
export default function HomeNavbar() {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@@ -863,7 +579,7 @@ export default function HomeNavbar() {
|
|||||||
{/* Logo - 价小前投研 */}
|
{/* Logo - 价小前投研 */}
|
||||||
<BrandLogo />
|
<BrandLogo />
|
||||||
|
|
||||||
{/* 中间导航区域 - 响应式 */}
|
{/* 中间导航区域 - 响应式 (Phase 4 优化) */}
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
// 移动端:汉堡菜单
|
// 移动端:汉堡菜单
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -874,10 +590,10 @@ export default function HomeNavbar() {
|
|||||||
/>
|
/>
|
||||||
) : isTablet ? (
|
) : isTablet ? (
|
||||||
// 中屏(平板):"更多"下拉菜单
|
// 中屏(平板):"更多"下拉菜单
|
||||||
<MoreNavMenu isAuthenticated={isAuthenticated} user={user} />
|
<MoreMenu isAuthenticated={isAuthenticated} user={user} />
|
||||||
) : (
|
) : (
|
||||||
// 大屏(桌面):完整导航菜单
|
// 大屏(桌面):完整导航菜单
|
||||||
<NavItems isAuthenticated={isAuthenticated} user={user} />
|
<DesktopNav isAuthenticated={isAuthenticated} user={user} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 右侧:日夜模式切换 + 登录/用户区 */}
|
{/* 右侧:日夜模式切换 + 登录/用户区 */}
|
||||||
@@ -1081,53 +797,9 @@ export default function HomeNavbar() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 个人中心下拉菜单 - 仅大屏显示 */}
|
{/* 个人中心下拉菜单 - 仅大屏显示 (Phase 4 优化) */}
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<Menu>
|
<PersonalCenterMenu user={user} handleLogout={handleLogout} />
|
||||||
<MenuButton
|
|
||||||
as={Button}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
rightIcon={<ChevronDownIcon />}
|
|
||||||
_hover={{ bg: useColorModeValue('gray.100', 'gray.700') }}
|
|
||||||
>
|
|
||||||
个人中心
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
|
||||||
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
|
||||||
{user.phone && (
|
|
||||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
|
||||||
)}
|
|
||||||
{user.has_wechat && (
|
|
||||||
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{/* 前往个人中心 */}
|
|
||||||
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
|
|
||||||
前往个人中心
|
|
||||||
</MenuItem>
|
|
||||||
<MenuDivider />
|
|
||||||
{/* 账户管理组 */}
|
|
||||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
|
||||||
个人资料
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
|
||||||
账户设置
|
|
||||||
</MenuItem>
|
|
||||||
<MenuDivider />
|
|
||||||
{/* 功能入口组 */}
|
|
||||||
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
|
||||||
订阅管理
|
|
||||||
</MenuItem>
|
|
||||||
<MenuDivider />
|
|
||||||
{/* 退出 */}
|
|
||||||
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
|
||||||
退出登录
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
200
src/components/Navbars/components/Navigation/DesktopNav.js
Normal file
200
src/components/Navbars/components/Navigation/DesktopNav.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// src/components/Navbars/components/Navigation/DesktopNav.js
|
||||||
|
// 桌面版主导航菜单 - 完整的导航栏
|
||||||
|
|
||||||
|
import React, { memo, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
Badge,
|
||||||
|
useColorModeValue
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useNavigationEvents } from '../../../../hooks/useNavigationEvents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 桌面版主导航菜单组件
|
||||||
|
* 大屏幕 (lg+) 显示,包含完整的下拉导航菜单
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {boolean} props.isAuthenticated - 用户是否已登录
|
||||||
|
* @param {Object} props.user - 用户信息
|
||||||
|
*/
|
||||||
|
const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// ⚠️ 必须在组件顶层调用所有Hooks(不能在JSX中调用)
|
||||||
|
const contactTextColor = useColorModeValue('gray.500', 'gray.300');
|
||||||
|
|
||||||
|
// 🎯 初始化导航埋点Hook
|
||||||
|
const navEvents = useNavigationEvents({ component: 'top_nav' });
|
||||||
|
|
||||||
|
// 辅助函数:判断导航项是否激活
|
||||||
|
const isActive = useCallback((paths) => {
|
||||||
|
return paths.some(path => location.pathname.includes(path));
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
if (!isAuthenticated || !user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack spacing={8}>
|
||||||
|
{/* 高频跟踪 */}
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
rightIcon={<ChevronDownIcon />}
|
||||||
|
bg={isActive(['/community', '/concepts']) ? 'blue.50' : 'transparent'}
|
||||||
|
color={isActive(['/community', '/concepts']) ? 'blue.600' : 'inherit'}
|
||||||
|
fontWeight={isActive(['/community', '/concepts']) ? 'bold' : 'normal'}
|
||||||
|
borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
_hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }}
|
||||||
|
>
|
||||||
|
高频跟踪
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList minW="260px" p={2}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
// 🎯 追踪菜单项点击
|
||||||
|
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
|
||||||
|
navigate('/community');
|
||||||
|
}}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
||||||
|
borderLeft={location.pathname.includes('/community') ? '3px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
fontWeight={location.pathname.includes('/community') ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">事件中心</Text>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
// 🎯 追踪菜单项点击
|
||||||
|
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
|
||||||
|
navigate('/concepts');
|
||||||
|
}}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
||||||
|
borderLeft={location.pathname.includes('/concepts') ? '3px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
fontWeight={location.pathname.includes('/concepts') ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">概念中心</Text>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{/* 行情复盘 */}
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
rightIcon={<ChevronDownIcon />}
|
||||||
|
bg={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.50' : 'transparent'}
|
||||||
|
color={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.600' : 'inherit'}
|
||||||
|
fontWeight={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'bold' : 'normal'}
|
||||||
|
borderBottom={isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? '2px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
_hover={{ bg: isActive(['/limit-analyse', '/stocks', '/trading-simulation']) ? 'blue.100' : 'gray.50' }}
|
||||||
|
>
|
||||||
|
行情复盘
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList minW="260px" p={2}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/limit-analyse')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||||
|
borderLeft={location.pathname.includes('/limit-analyse') ? '3px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
fontWeight={location.pathname.includes('/limit-analyse') ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">涨停分析</Text>
|
||||||
|
<Badge size="sm" colorScheme="blue">FREE</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/stocks')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||||
|
borderLeft={location.pathname.includes('/stocks') ? '3px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
fontWeight={location.pathname.includes('/stocks') ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">个股中心</Text>
|
||||||
|
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/trading-simulation')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||||
|
borderLeft={location.pathname.includes('/trading-simulation') ? '3px solid' : 'none'}
|
||||||
|
borderColor="blue.600"
|
||||||
|
fontWeight={location.pathname.includes('/trading-simulation') ? 'bold' : 'normal'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">模拟盘</Text>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{/* AGENT社群 */}
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
||||||
|
AGENT社群
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList minW="300px" p={4}>
|
||||||
|
<MenuItem
|
||||||
|
isDisabled
|
||||||
|
cursor="not-allowed"
|
||||||
|
color="gray.400"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" color="gray.400">今日热议</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
isDisabled
|
||||||
|
cursor="not-allowed"
|
||||||
|
color="gray.400"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" color="gray.400">个股社区</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{/* 联系我们 */}
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} variant="ghost" rightIcon={<ChevronDownIcon />}>
|
||||||
|
联系我们
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList minW="260px" p={4}>
|
||||||
|
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DesktopNav.displayName = 'DesktopNav';
|
||||||
|
|
||||||
|
export default DesktopNav;
|
||||||
135
src/components/Navbars/components/Navigation/MoreMenu.js
Normal file
135
src/components/Navbars/components/Navigation/MoreMenu.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// src/components/Navbars/components/Navigation/MoreMenu.js
|
||||||
|
// 平板版"更多"下拉菜单 - 包含所有导航项
|
||||||
|
|
||||||
|
import React, { memo, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Badge
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平板版"更多"下拉菜单组件
|
||||||
|
* 中屏幕 (sm-md) 显示,包含所有导航项的折叠菜单
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {boolean} props.isAuthenticated - 用户是否已登录
|
||||||
|
* @param {Object} props.user - 用户信息
|
||||||
|
*/
|
||||||
|
const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// 辅助函数:判断导航项是否激活
|
||||||
|
const isActive = useCallback((paths) => {
|
||||||
|
return paths.some(path => location.pathname.includes(path));
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
if (!isAuthenticated || !user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
rightIcon={<ChevronDownIcon />}
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
更多
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList minW="300px" p={2}>
|
||||||
|
{/* 高频跟踪组 */}
|
||||||
|
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/community')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">事件中心</Text>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/concepts')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">概念中心</Text>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* 行情复盘组 */}
|
||||||
|
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/limit-analyse')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">涨停分析</Text>
|
||||||
|
<Badge size="sm" colorScheme="blue">FREE</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/stocks')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">个股中心</Text>
|
||||||
|
<Badge size="sm" colorScheme="green">HOT</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => navigate('/trading-simulation')}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" w="100%">
|
||||||
|
<Text fontSize="sm">模拟盘</Text>
|
||||||
|
<Badge size="sm" colorScheme="red">NEW</Badge>
|
||||||
|
</Flex>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* AGENT社群组 */}
|
||||||
|
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">AGENT社群</Text>
|
||||||
|
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||||
|
<Text fontSize="sm" color="gray.400">今日热议</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||||
|
<Text fontSize="sm" color="gray.400">个股社区</Text>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* 联系我们 */}
|
||||||
|
<MenuItem isDisabled cursor="not-allowed" color="gray.400">
|
||||||
|
<Text fontSize="sm" color="gray.400">联系我们</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MoreMenu.displayName = 'MoreMenu';
|
||||||
|
|
||||||
|
export default MoreMenu;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// src/components/Navbars/components/Navigation/PersonalCenterMenu.js
|
||||||
|
// 个人中心下拉菜单 - 仅桌面版显示
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
useColorModeValue
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
|
import { FiHome, FiUser, FiSettings, FiLogOut } from 'react-icons/fi';
|
||||||
|
import { FaCrown } from 'react-icons/fa';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 个人中心下拉菜单组件
|
||||||
|
* 仅在桌面版 (lg+) 显示
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} props.user - 用户信息
|
||||||
|
* @param {Function} props.handleLogout - 退出登录回调
|
||||||
|
*/
|
||||||
|
const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const hoverBg = useColorModeValue('gray.100', 'gray.700');
|
||||||
|
|
||||||
|
// 获取显示名称
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (user.nickname) return user.nickname;
|
||||||
|
if (user.username) return user.username;
|
||||||
|
if (user.email) return user.email.split('@')[0];
|
||||||
|
if (user.phone) return user.phone;
|
||||||
|
return '用户';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
rightIcon={<ChevronDownIcon />}
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
|
>
|
||||||
|
个人中心
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{/* 用户信息区 */}
|
||||||
|
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||||
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">{user.email}</Text>
|
||||||
|
{user.phone && (
|
||||||
|
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||||
|
)}
|
||||||
|
{user.has_wechat && (
|
||||||
|
<Badge size="sm" colorScheme="green" mt={1}>微信已绑定</Badge>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 前往个人中心 */}
|
||||||
|
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
|
||||||
|
前往个人中心
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* 账户管理组 */}
|
||||||
|
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||||
|
个人资料
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||||
|
账户设置
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* 功能入口组 */}
|
||||||
|
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
||||||
|
订阅管理
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
{/* 退出 */}
|
||||||
|
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
|
||||||
|
退出登录
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PersonalCenterMenu.displayName = 'PersonalCenterMenu';
|
||||||
|
|
||||||
|
export default PersonalCenterMenu;
|
||||||
6
src/components/Navbars/components/Navigation/index.js
Normal file
6
src/components/Navbars/components/Navigation/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/components/Navbars/components/Navigation/index.js
|
||||||
|
// 导航组件统一导出
|
||||||
|
|
||||||
|
export { default as DesktopNav } from './DesktopNav';
|
||||||
|
export { default as MoreMenu } from './MoreMenu';
|
||||||
|
export { default as PersonalCenterMenu } from './PersonalCenterMenu';
|
||||||
Reference in New Issue
Block a user