refactor(navbar): 导航栏布局重构

- 新增 MySpaceButton 组件
  - 删除 PersonalCenterMenu,功能合并到 DesktopUserMenu
  - 桌面端布局:[我的空间] | [头像][用户名]
This commit is contained in:
zdl
2026-01-13 14:58:29 +08:00
parent 61df9af798
commit 7148dd97c2
6 changed files with 286 additions and 194 deletions

View File

@@ -32,10 +32,14 @@ const CalendarButton = memo(() => {
<>
<Button
size="sm"
colorScheme="blue"
variant="solid"
borderRadius="full"
variant="ghost"
color="gray.700"
fontWeight="medium"
leftIcon={<Calendar size={16} />}
_hover={{
bg: 'gray.100',
color: 'gray.900',
}}
onClick={() => setIsModalOpen(true)}
>
投资日历

View File

@@ -2,13 +2,12 @@
// Navbar 右侧功能区组件
import React, { memo } from 'react';
import { HStack, IconButton, Box } from '@chakra-ui/react';
import { HStack, IconButton, Box, Divider } from '@chakra-ui/react';
import { Menu } from 'lucide-react';
// import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换
import LoginButton from '../LoginButton';
import CalendarButton from '../CalendarButton';
// import CalendarButton from '../CalendarButton'; // 暂时注释
import { DesktopUserMenu, TabletUserMenu } from '../UserMenu';
import { PersonalCenterMenu, MoreMenu } from '../Navigation';
import { MySpaceButton, MoreMenu } from '../Navigation';
/**
* Navbar 右侧功能区组件
@@ -48,35 +47,38 @@ const NavbarActions = memo(({
) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列
<HStack spacing={{ base: 2, md: 3 }}>
{/* 投资日历 - 仅大屏显示 */}
{isDesktop && <CalendarButton />}
{/* 投资日历 - 暂时注释 */}
{/* {isDesktop && <CalendarButton />} */}
{/* 头像区域 - 响应式 */}
{/* 桌面端布局:[我的空间] | [头像][用户名] */}
{isDesktop ? (
<DesktopUserMenu user={user} />
) : (
<TabletUserMenu
user={user}
handleLogout={handleLogout}
/>
)}
{/* 头像右侧的菜单 - 响应式(互斥逻辑,确保只渲染一个) */}
{isDesktop ? (
// 桌面端:个人中心下拉菜单
<PersonalCenterMenu user={user} handleLogout={handleLogout} />
<>
<MySpaceButton />
<Divider
orientation="vertical"
h="24px"
borderColor="gray.300"
/>
<DesktopUserMenu user={user} handleLogout={handleLogout} />
</>
) : isTablet ? (
// 平板端MoreMenu 下拉菜单
<MoreMenu isAuthenticated={isAuthenticated} user={user} />
// 平板端:头像 + MoreMenu
<>
<TabletUserMenu user={user} handleLogout={handleLogout} />
<MoreMenu isAuthenticated={isAuthenticated} user={user} />
</>
) : (
// 移动端:汉堡菜单(打开抽屉)
<IconButton
icon={<Menu size={20} />}
variant="ghost"
onClick={onMenuOpen}
aria-label="打开菜单"
size="md"
/>
// 移动端:头像 + 汉堡菜单
<>
<TabletUserMenu user={user} handleLogout={handleLogout} />
<IconButton
icon={<Menu size={20} />}
variant="ghost"
onClick={onMenuOpen}
aria-label="打开菜单"
size="md"
/>
</>
)}
</HStack>
) : (

View File

@@ -0,0 +1,38 @@
// src/components/Navbars/components/Navigation/MySpaceButton.js
// 「我的空间」独立跳转按钮 - 点击直接跳转至个人中心
import React, { memo } from 'react';
import { Button } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
/**
* 「我的空间」独立按钮组件
* 点击直接跳转至个人中心页面
*
* 黑金主题配色:
* - 默认:金色文字 (#CC9C00)
* - hover浅金背景 (#FFF9E6) + 深金文字 (#997500)
*/
const MySpaceButton = memo(() => {
const navigate = useNavigate();
return (
<Button
size="sm"
variant="ghost"
color="gray.700"
fontWeight="medium"
_hover={{
bg: 'gray.100',
color: 'gray.900',
}}
onClick={() => navigate('/home/center')}
>
我的主页
</Button>
);
});
MySpaceButton.displayName = 'MySpaceButton';
export default MySpaceButton;

View File

@@ -1,116 +0,0 @@
// src/components/Navbars/components/Navigation/PersonalCenterMenu.js
// 个人中心下拉菜单 - 仅桌面版显示
import React, { memo } from 'react';
import {
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
Button,
Box,
Text,
Badge,
useDisclosure
} from '@chakra-ui/react';
import { ChevronDown, Home, User, Settings, LogOut, Crown } from 'lucide-react';
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();
// 🎯 为个人中心菜单创建 useDisclosure Hook
const { isOpen, onOpen, onClose } = useDisclosure();
// 获取显示名称
const getDisplayName = () => {
if (user.nickname) return user.nickname;
if (user.username) return user.username;
if (user.email) return user.email.split('@')[0];
if (typeof user.phone === 'string' && user.phone) return user.phone;
return '用户';
};
return (
<Menu isOpen={isOpen} onClose={onClose}>
<MenuButton
as={Button}
size="sm"
colorScheme="blue"
variant="solid"
borderRadius="full"
rightIcon={<ChevronDown size={16} />}
onMouseEnter={onOpen}
onMouseLeave={onClose}
>
个人中心
</MenuButton>
<MenuList onMouseEnter={onOpen}>
{/* 用户信息区 */}
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
{typeof user.phone === 'string' && 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={<Home size={16} />} onClick={() => {
onClose(); // 先关闭菜单
navigate('/home/center');
}}>
前往个人中心
</MenuItem>
<MenuDivider />
{/* 账户管理组 */}
<MenuItem icon={<User size={16} />} onClick={() => {
onClose(); // 先关闭菜单
navigate('/home/profile');
}}>
个人资料
</MenuItem>
<MenuItem icon={<Settings size={16} />} onClick={() => {
onClose(); // 先关闭菜单
navigate('/home/settings');
}}>
账户设置
</MenuItem>
<MenuDivider />
{/* 功能入口组 */}
<MenuItem icon={<Crown size={16} />} onClick={() => {
onClose(); // 先关闭菜单
navigate('/home/pages/account/subscription');
}}>
订阅管理
</MenuItem>
<MenuDivider />
{/* 退出 */}
<MenuItem icon={<LogOut size={16} />} onClick={handleLogout} color="red.500">
退出登录
</MenuItem>
</MenuList>
</Menu>
);
});
PersonalCenterMenu.displayName = 'PersonalCenterMenu';
export default PersonalCenterMenu;

View File

@@ -3,4 +3,5 @@
export { default as DesktopNav } from './DesktopNav';
export { default as MoreMenu } from './MoreMenu';
export { default as PersonalCenterMenu } from './PersonalCenterMenu';
export { default as MySpaceButton } from './MySpaceButton';
// PersonalCenterMenu 已废弃,功能合并到 DesktopUserMenu

View File

@@ -1,71 +1,234 @@
// src/components/Navbars/components/UserMenu/DesktopUserMenu.js
// 桌面版用户菜单 - 头像点击跳转到订阅页面
// 桌面版用户菜单 - 头像+用户名组合,点击展开综合下拉面板
import React, { memo } from 'react';
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverArrow,
useColorModeValue
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
Box,
HStack,
VStack,
Text,
Button,
useDisclosure
} from '@chakra-ui/react';
import { Settings, LogOut } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import UserAvatar from './UserAvatar';
import { TooltipContent } from '../../../Subscription/CrownTooltip';
import { useSubscription } from '../../../../hooks/useSubscription';
/**
* 桌面版用户菜单组件
* 大屏幕 (md+) 显示,头像点击跳转到订阅页面
*
* @param {Object} props
* @param {Object} props.user - 用户信息
* 会员权益条组件
* 金色渐变背景,单行显示会员类型和到期时间
*/
const DesktopUserMenu = memo(({ user }) => {
const MembershipBar = memo(({ subscriptionInfo, onClose }) => {
const navigate = useNavigate();
const { subscriptionInfo } = useSubscription();
const { type, days_left } = subscriptionInfo;
const popoverBg = useColorModeValue('white', 'gray.800');
const popoverBorderColor = useColorModeValue('gray.200', 'gray.600');
const getMemberText = () => {
if (type === 'free') return '基础版';
if (type === 'pro') return 'Pro会员';
return 'Max会员';
};
const handleAvatarClick = () => {
const getMemberIcon = () => {
if (type === 'free') return '✨';
if (type === 'pro') return '💎';
return '👑';
};
const handleClick = () => {
onClose();
navigate('/home/pages/account/subscription');
};
// 金色渐变背景
const gradientBg = type === 'free'
? 'linear(to-r, gray.100, gray.200)'
: 'linear(to-r, #F6E5A3, #D4AF37)';
return (
<Popover
trigger="hover"
placement="bottom-end"
openDelay={100}
closeDelay={200}
gutter={8}
<Box
px={4}
py={2.5}
bgGradient={gradientBg}
cursor="pointer"
onClick={handleClick}
_hover={{ opacity: 0.9 }}
>
<PopoverTrigger>
<span>
<HStack justify="space-between">
<HStack spacing={1}>
<Text fontSize="sm">{getMemberIcon()}</Text>
<Text fontSize="sm" fontWeight="600" color={type === 'free' ? 'gray.700' : 'gray.800'}>
{getMemberText()}
{type !== 'free' && (
<Text as="span" fontWeight="normal" color="gray.700">
{' '}· {days_left}天后到期
</Text>
)}
</Text>
</HStack>
<Button
size="xs"
variant="outline"
borderColor={type === 'free' ? 'gray.400' : 'gray.700'}
color={type === 'free' ? 'gray.600' : 'gray.800'}
bg="transparent"
_hover={{ bg: 'whiteAlpha.500' }}
onClick={(e) => {
e.stopPropagation();
handleClick();
}}
>
{type === 'free' ? '升级会员' : '管理订阅'}
</Button>
</HStack>
</Box>
);
});
MembershipBar.displayName = 'MembershipBar';
/**
* 桌面版用户菜单组件
* 头像+用户名组合(去掉箭头),点击展开综合下拉面板
*
* 布局: [头像][用户名]
* 交互: hover 时显示浅色圆角背景,点击展开面板
*
* @param {Object} props
* @param {Object} props.user - 用户信息
* @param {Function} props.handleLogout - 退出登录回调
*/
const DesktopUserMenu = memo(({ user, handleLogout }) => {
const navigate = useNavigate();
const { subscriptionInfo } = useSubscription();
const { isOpen, onOpen, onClose } = useDisclosure();
// 获取显示名称(含手机号脱敏逻辑)
const getDisplayName = () => {
// 1. 优先显示昵称
if (user.nickname) return user.nickname;
// 2. 其次显示用户名
if (user.username) return user.username;
// 3. 手机号脱敏
if (typeof user.phone === 'string' && user.phone) {
return user.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 4. 默认显示
return '股票新用户';
};
// 跳转到我的空间
const handleNavigateToMySpace = () => {
onClose();
navigate('/home/center');
};
// 跳转到账户设置
const handleNavigateToSettings = () => {
onClose();
navigate('/home/settings');
};
// 退出登录
const handleLogoutClick = () => {
onClose();
handleLogout();
};
return (
<Menu isOpen={isOpen} onClose={onClose}>
<MenuButton
as={Box}
px={2}
py={1}
borderRadius="full"
cursor="pointer"
onClick={onOpen}
transition="all 0.2s"
_hover={{
bg: 'rgba(0, 0, 0, 0.05)',
}}
>
{/* 使用 HStack 明确实现水平布局 */}
<HStack spacing={2}>
<UserAvatar
user={user}
subscriptionInfo={subscriptionInfo}
onClick={handleAvatarClick}
size="sm"
/>
</span>
</PopoverTrigger>
<PopoverContent
bg={popoverBg}
borderRadius="lg"
border="1px solid"
borderColor={popoverBorderColor}
boxShadow="lg"
p={3}
w="auto"
_focus={{ outline: 'none' }}
>
<PopoverArrow bg={popoverBg} />
<TooltipContent
subscriptionInfo={subscriptionInfo}
onNavigate={handleAvatarClick}
/>
</PopoverContent>
</Popover>
<Text
fontSize="sm"
fontWeight="medium"
maxW="80px"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
color="gray.700"
>
{getDisplayName()}
</Text>
</HStack>
</MenuButton>
<MenuList minW="280px" py={0} overflow="hidden" borderRadius="lg">
{/* 顶部:用户信息区 - 深色背景 + 头像 + 用户名 */}
<Box
px={4}
py={4}
bg="gray.800"
cursor="pointer"
_hover={{ bg: 'gray.700' }}
onClick={handleNavigateToMySpace}
>
<HStack spacing={3}>
<UserAvatar
user={user}
subscriptionInfo={subscriptionInfo}
size="md"
/>
<VStack align="start" spacing={0}>
<Text fontWeight="bold" fontSize="md" color="white">
{getDisplayName()}
</Text>
<Text fontSize="xs" color="gray.400">
ID: {user.phone || user.id || '---'}
</Text>
</VStack>
</HStack>
</Box>
{/* 会员权益条 - 金色渐变背景 */}
<MembershipBar subscriptionInfo={subscriptionInfo} onClose={onClose} />
<MenuDivider my={0} />
{/* 列表区:快捷功能 */}
<MenuItem
icon={<Settings size={16} />}
onClick={handleNavigateToSettings}
py={3}
>
账户设置
</MenuItem>
<MenuDivider my={0} />
{/* 底部:退出登录 */}
<MenuItem
icon={<LogOut size={16} />}
color="red.500"
onClick={handleLogoutClick}
py={3}
>
退出登录
</MenuItem>
</MenuList>
</Menu>
);
});