refactor(HomeNavbar): Phase 5 - 提取移动端抽屉菜单组件

**背景**
继 Phase 1-4 后,进一步优化 HomeNavbar 的移动端菜单结构

**重构内容**
1. **新增组件目录** `src/components/Navbars/components/MobileDrawer/`
   - MobileDrawer.js (314行) - 移动端完整抽屉菜单
     * 用户信息展示
     * 日夜模式切换
     * 完整导航菜单(高频跟踪、行情复盘、AGENT社群、联系我们)
     * 登录/退出登录按钮
   - index.js - 统一导出

2. **HomeNavbar.js 优化**
   - 删除 ~262 行移动端 Drawer JSX 代码
   - 精简 Chakra UI 导入(移除 Drawer、DrawerBody、DrawerHeader 等 12 个组件)
   - 替换为 MobileDrawer 组件调用
   - 1065 → 815 行 (-250行, -23%)

**技术亮点**
- React.memo 优化渲染性能
- 封装导航点击逻辑(handleNavigate)
- 独立管理主题切换状态
- 响应式颜色模式(useColorModeValue)
- 完整的用户状态判断和 UI 展示

**累计成果** (Phase 1-5)
- 原始: 1623 行
- 当前: 815 行
- 减少: 808 行 (-50%)
- 提取: 11 个组件

🤖 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:42:14 +08:00
parent 89e5e60a6a
commit 60aa4c5c60
3 changed files with 330 additions and 262 deletions

View File

@@ -6,37 +6,20 @@ import {
Button, Button,
Container, Container,
useDisclosure, useDisclosure,
Drawer,
DrawerBody,
DrawerHeader,
DrawerOverlay,
DrawerContent,
DrawerCloseButton,
VStack,
HStack, HStack,
Icon, Icon,
Menu, Menu,
MenuButton, MenuButton,
MenuList, MenuList,
MenuItem, MenuItem,
MenuDivider,
Badge, Badge,
Grid, Grid,
IconButton, IconButton,
useBreakpointValue, useBreakpointValue,
Link,
Divider,
Avatar,
Spinner, Spinner,
useColorMode, useColorMode,
useColorModeValue, useColorModeValue,
useToast, useToast,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons';
import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi'; import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi';
@@ -63,6 +46,9 @@ import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu';
// Phase 4 优化: 提取的导航菜单组件 // Phase 4 优化: 提取的导航菜单组件
import { DesktopNav, MoreMenu, PersonalCenterMenu } from './components/Navigation'; import { DesktopNav, MoreMenu, PersonalCenterMenu } from './components/Navigation';
// Phase 5 优化: 提取的移动端抽屉菜单组件
import { MobileDrawer } from './components/MobileDrawer';
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ /** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
const SecondaryNav = ({ showCompletenessAlert }) => { const SecondaryNav = ({ showCompletenessAlert }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -810,251 +796,15 @@ export default function HomeNavbar() {
</Flex> </Flex>
</Container> </Container>
{/* 移动端抽屉菜单 */} {/* 移动端抽屉菜单 (Phase 5 优化) */}
<Drawer isOpen={isOpen} placement="right" onClose={onClose}> <MobileDrawer
<DrawerOverlay /> isOpen={isOpen}
<DrawerContent> onClose={onClose}
<DrawerCloseButton /> isAuthenticated={isAuthenticated}
<DrawerHeader> user={user}
<HStack> handleLogout={handleLogout}
<Text>菜单</Text> openAuthModal={openAuthModal}
{isAuthenticated && user && ( />
<Badge colorScheme="green" ml={2}>已登录</Badge>
)}
</HStack>
</DrawerHeader>
<DrawerBody>
<VStack spacing={4} align="stretch">
{/* 移动端:日夜模式切换 */}
<Button
leftIcon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
variant="ghost"
justifyContent="flex-start"
onClick={toggleColorMode}
size="sm"
>
切换到{colorMode === 'light' ? '深色' : '浅色'}模式
</Button>
{/* 移动端用户信息 */}
{isAuthenticated && user && (
<>
<Box p={3} bg={useColorModeValue('gray.50', 'whiteAlpha.100')} borderRadius="md">
<HStack>
<Avatar
size="sm"
name={getDisplayName()}
src={user.avatar_url}
bg="blue.500"
/>
<Box>
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
<Text fontSize="xs" color={useColorModeValue('gray.500', 'gray.300')}>{user.email}</Text>
</Box>
</HStack>
</Box>
<Divider />
</>
)}
{/* 首页链接 */}
<Link
onClick={() => {
navigate('/home');
onClose();
}}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
color="blue.500"
fontWeight="bold"
bg={location.pathname === '/home' ? 'blue.50' : 'transparent'}
borderLeft={location.pathname === '/home' ? '3px solid' : 'none'}
borderColor="blue.600"
>
<Text fontSize="md">🏠 首页</Text>
</Link>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>高频跟踪</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => {
navigate('/community');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">事件中心</Text>
<HStack spacing={1}>
<Badge size="xs" colorScheme="green">HOT</Badge>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</HStack>
</Link>
<Link
onClick={() => {
navigate('/concepts');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">概念中心</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>行情复盘</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => {
navigate('/limit-analyse');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">涨停分析</Text>
<Badge size="xs" colorScheme="blue">FREE</Badge>
</HStack>
</Link>
<Link
onClick={() => {
navigate('/stocks');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">个股中心</Text>
<Badge size="xs" colorScheme="green">HOT</Badge>
</HStack>
</Link>
<Link
onClick={() => {
navigate('/trading-simulation');
onClose();
}}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">模拟盘</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>AGENT社群</Text>
<VStack spacing={2} align="stretch">
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">今日热议</Text>
</Link>
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">个股社区</Text>
</Link>
</VStack>
</Box>
<Divider />
<Box>
<Text fontWeight="bold" mb={2}>联系我们</Text>
<Text fontSize="sm" color={useColorModeValue('gray.500', 'gray.300')}>敬请期待</Text>
</Box>
{/* 移动端登录/登出按钮 */}
<Divider />
{isAuthenticated && user ? (
<Button
colorScheme="red"
variant="outline"
size="sm"
onClick={() => {
handleLogout();
onClose();
}}
>
🚪 退出登录
</Button>
) : (
<Button
colorScheme="blue"
size="sm"
onClick={() => {
openAuthModal();
onClose();
}}
>
🔐 登录 / 注册
</Button>
)}
</VStack>
</DrawerBody>
</DrawerContent>
</Drawer>
</Box> </Box>
{/* 二级导航栏 - 显示当前页面所属的二级菜单 */} {/* 二级导航栏 - 显示当前页面所属的二级菜单 */}

View File

@@ -0,0 +1,314 @@
// src/components/Navbars/components/MobileDrawer/MobileDrawer.js
// 移动端抽屉菜单组件
import React, { memo } from 'react';
import {
Drawer,
DrawerOverlay,
DrawerContent,
DrawerCloseButton,
DrawerHeader,
DrawerBody,
VStack,
HStack,
Box,
Text,
Button,
Badge,
Link,
Divider,
Avatar,
useColorMode,
useColorModeValue
} from '@chakra-ui/react';
import { SunIcon, MoonIcon } from '@chakra-ui/icons';
import { useNavigate, useLocation } from 'react-router-dom';
/**
* 移动端抽屉菜单组件
* 包含完整的导航菜单和用户功能
*
* @param {Object} props
* @param {boolean} props.isOpen - Drawer 是否打开
* @param {Function} props.onClose - 关闭 Drawer 的回调
* @param {boolean} props.isAuthenticated - 用户是否已登录
* @param {Object} props.user - 用户信息
* @param {Function} props.handleLogout - 退出登录回调
* @param {Function} props.openAuthModal - 打开登录弹窗回调
*/
const MobileDrawer = memo(({
isOpen,
onClose,
isAuthenticated,
user,
handleLogout,
openAuthModal
}) => {
const navigate = useNavigate();
const location = useLocation();
const { colorMode, toggleColorMode } = useColorMode();
const userBgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
const contactTextColor = useColorModeValue('gray.500', 'gray.300');
const emailTextColor = useColorModeValue('gray.500', 'gray.300');
// 获取显示名称
const getDisplayName = () => {
if (!user) return '用户';
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 '用户';
};
// 导航点击处理
const handleNavigate = (path) => {
navigate(path);
onClose();
};
return (
<Drawer isOpen={isOpen} placement="right" onClose={onClose}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>
<HStack>
<Text>菜单</Text>
{isAuthenticated && user && (
<Badge colorScheme="green" ml={2}>已登录</Badge>
)}
</HStack>
</DrawerHeader>
<DrawerBody>
<VStack spacing={4} align="stretch">
{/* 移动端:日夜模式切换 */}
<Button
leftIcon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
variant="ghost"
justifyContent="flex-start"
onClick={toggleColorMode}
size="sm"
>
切换到{colorMode === 'light' ? '深色' : '浅色'}模式
</Button>
{/* 移动端用户信息 */}
{isAuthenticated && user && (
<>
<Box p={3} bg={userBgColor} borderRadius="md">
<HStack>
<Avatar
size="sm"
name={getDisplayName()}
src={user.avatar_url}
bg="blue.500"
/>
<Box>
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
<Text fontSize="xs" color={emailTextColor}>{user.email}</Text>
</Box>
</HStack>
</Box>
<Divider />
</>
)}
{/* 首页链接 */}
<Link
onClick={() => handleNavigate('/home')}
py={2}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
color="blue.500"
fontWeight="bold"
bg={location.pathname === '/home' ? 'blue.50' : 'transparent'}
borderLeft={location.pathname === '/home' ? '3px solid' : 'none'}
borderColor="blue.600"
>
<Text fontSize="md">🏠 首页</Text>
</Link>
<Divider />
{/* 高频跟踪 */}
<Box>
<Text fontWeight="bold" mb={2}>高频跟踪</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => handleNavigate('/community')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">事件中心</Text>
<HStack spacing={1}>
<Badge size="xs" colorScheme="green">HOT</Badge>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</HStack>
</Link>
<Link
onClick={() => handleNavigate('/concepts')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">概念中心</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
{/* 行情复盘 */}
<Box>
<Text fontWeight="bold" mb={2}>行情复盘</Text>
<VStack spacing={2} align="stretch">
<Link
onClick={() => handleNavigate('/limit-analyse')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">涨停分析</Text>
<Badge size="xs" colorScheme="blue">FREE</Badge>
</HStack>
</Link>
<Link
onClick={() => handleNavigate('/stocks')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">个股中心</Text>
<Badge size="xs" colorScheme="green">HOT</Badge>
</HStack>
</Link>
<Link
onClick={() => handleNavigate('/trading-simulation')}
py={1}
px={3}
borderRadius="md"
_hover={{ bg: 'gray.100' }}
cursor="pointer"
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'}
>
<HStack justify="space-between">
<Text fontSize="sm">模拟盘</Text>
<Badge size="xs" colorScheme="red">NEW</Badge>
</HStack>
</Link>
</VStack>
</Box>
<Divider />
{/* AGENT社群 */}
<Box>
<Text fontWeight="bold" mb={2}>AGENT社群</Text>
<VStack spacing={2} align="stretch">
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">今日热议</Text>
</Link>
<Link
py={1}
px={3}
borderRadius="md"
_hover={{}}
cursor="not-allowed"
color="gray.400"
pointerEvents="none"
>
<Text fontSize="sm" color="gray.400">个股社区</Text>
</Link>
</VStack>
</Box>
<Divider />
{/* 联系我们 */}
<Box>
<Text fontWeight="bold" mb={2}>联系我们</Text>
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
</Box>
{/* 移动端登录/登出按钮 */}
<Divider />
{isAuthenticated && user ? (
<Button
colorScheme="red"
variant="outline"
size="sm"
onClick={() => {
handleLogout();
onClose();
}}
>
🚪 退出登录
</Button>
) : (
<Button
colorScheme="blue"
size="sm"
onClick={() => {
openAuthModal();
onClose();
}}
>
🔐 登录 / 注册
</Button>
)}
</VStack>
</DrawerBody>
</DrawerContent>
</Drawer>
);
});
MobileDrawer.displayName = 'MobileDrawer';
export default MobileDrawer;

View File

@@ -0,0 +1,4 @@
// src/components/Navbars/components/MobileDrawer/index.js
// 移动端抽屉菜单组件统一导出
export { default as MobileDrawer } from './MobileDrawer';