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 优化: 提取的用户菜单组件
|
||||
import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu';
|
||||
|
||||
// Phase 4 优化: 提取的导航菜单组件
|
||||
import { DesktopNav, MoreMenu, PersonalCenterMenu } from './components/Navigation';
|
||||
|
||||
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
||||
const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -211,294 +214,7 @@ const SecondaryNav = ({ showCompletenessAlert }) => {
|
||||
};
|
||||
|
||||
/** 中屏"更多"菜单 - 用于平板和小笔记本 */
|
||||
const MoreNavMenu = ({ 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>
|
||||
);
|
||||
};
|
||||
|
||||
/** 桌面端导航 - 完全按照原网站
|
||||
* @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;
|
||||
}
|
||||
};
|
||||
// Phase 4: MoreNavMenu 和 NavItems 组件已提取到 Navigation 目录
|
||||
|
||||
export default function HomeNavbar() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
@@ -863,7 +579,7 @@ export default function HomeNavbar() {
|
||||
{/* Logo - 价小前投研 */}
|
||||
<BrandLogo />
|
||||
|
||||
{/* 中间导航区域 - 响应式 */}
|
||||
{/* 中间导航区域 - 响应式 (Phase 4 优化) */}
|
||||
{isMobile ? (
|
||||
// 移动端:汉堡菜单
|
||||
<IconButton
|
||||
@@ -874,10 +590,10 @@ export default function HomeNavbar() {
|
||||
/>
|
||||
) : 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 && (
|
||||
<Menu>
|
||||
<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>
|
||||
<PersonalCenterMenu user={user} handleLogout={handleLogout} />
|
||||
)}
|
||||
</HStack>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user