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:
zdl
2025-10-30 17:10:31 +08:00
parent 77440f78a7
commit 89e5e60a6a
5 changed files with 452 additions and 337 deletions

View File

@@ -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>
) : ( ) : (

View 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;

View 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;

View File

@@ -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;

View 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';