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 优化: 提取的用户菜单组件
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>
) : (