feat: 导航UI调整

This commit is contained in:
zdl
2025-10-21 15:43:35 +08:00
parent eef383f56f
commit 98653f042b
2 changed files with 405 additions and 55 deletions

View File

@@ -43,7 +43,7 @@ import { logger } from '../../utils/logger';
import { getApiBase } from '../../utils/apiConfig'; import { getApiBase } from '../../utils/apiConfig';
import SubscriptionButton from '../Subscription/SubscriptionButton'; import SubscriptionButton from '../Subscription/SubscriptionButton';
import SubscriptionModal from '../Subscription/SubscriptionModal'; import SubscriptionModal from '../Subscription/SubscriptionModal';
import CrownTooltip, { TooltipContent } from '../Subscription/CrownTooltip'; import { CrownIcon, TooltipContent } from '../Subscription/CrownTooltip';
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ /** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
const SecondaryNav = ({ showCompletenessAlert }) => { const SecondaryNav = ({ showCompletenessAlert }) => {
@@ -186,6 +186,112 @@ 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 * @TODO 添加逻辑 不展示导航case
* 1.未登陆状态 && 是首页 * 1.未登陆状态 && 是首页
@@ -360,6 +466,8 @@ export default function HomeNavbar() {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useBreakpointValue({ base: true, md: false }); const isMobile = useBreakpointValue({ base: true, md: false });
const isTablet = useBreakpointValue({ base: false, md: true, lg: false });
const isDesktop = useBreakpointValue({ base: false, md: false, lg: true });
const { user, isAuthenticated, logout, isLoading } = useAuth(); const { user, isAuthenticated, logout, isLoading } = useAuth();
const { openAuthModal } = useAuthModal(); const { openAuthModal } = useAuthModal();
const { colorMode, toggleColorMode } = useColorMode(); const { colorMode, toggleColorMode } = useColorMode();
@@ -746,15 +854,22 @@ export default function HomeNavbar() {
</Text> </Text>
</HStack> </HStack>
{/* 移动端菜单按钮 */} {/* 中间导航区域 - 响应式 */}
{isMobile ? ( {isMobile ? (
// 移动端:汉堡菜单
<IconButton <IconButton
icon={<HamburgerIcon />} icon={<HamburgerIcon />}
variant="ghost" variant="ghost"
onClick={onOpen} onClick={onOpen}
aria-label="Open menu" aria-label="Open menu"
/> />
) : <NavItems isAuthenticated={isAuthenticated} user={user} />} ) : isTablet ? (
// 中屏(平板):"更多"下拉菜单
<MoreNavMenu isAuthenticated={isAuthenticated} user={user} />
) : (
// 大屏(桌面):完整导航菜单
<NavItems isAuthenticated={isAuthenticated} user={user} />
)}
{/* 右侧:日夜模式切换 + 登录/用户区 */} {/* 右侧:日夜模式切换 + 登录/用户区 */}
<HStack spacing={{ base: 2, md: 4 }}> <HStack spacing={{ base: 2, md: 4 }}>
@@ -774,8 +889,8 @@ export default function HomeNavbar() {
) : isAuthenticated && user ? ( ) : isAuthenticated && user ? (
// 已登录状态 - 用户菜单 + 功能菜单排列 // 已登录状态 - 用户菜单 + 功能菜单排列
<HStack spacing={{ base: 2, md: 3 }}> <HStack spacing={{ base: 2, md: 3 }}>
{/* 自选股 - 移动端隐藏 */} {/* 自选股 - 仅大屏显示 */}
{!isMobile && ( {isDesktop && (
<Menu onOpen={loadWatchlistQuotes}> <Menu onOpen={loadWatchlistQuotes}>
<MenuButton <MenuButton
as={Button} as={Button}
@@ -852,8 +967,8 @@ export default function HomeNavbar() {
</Menu> </Menu>
)} )}
{/* 关注的事件 - 头像右侧 - 移动端隐藏 */} {/* 关注的事件 - 仅大屏显示 */}
{!isMobile && ( {isDesktop && (
<Menu onOpen={loadFollowingEvents}> <Menu onOpen={loadFollowingEvents}>
<MenuButton <MenuButton
as={Button} as={Button}
@@ -936,16 +1051,10 @@ export default function HomeNavbar() {
</Menu> </Menu>
)} )}
{/* 带会员标识的头像 - 点击打开订阅弹窗 */} {/* 头像区域 - 响应式 */}
<Box {isDesktop ? (
position="relative" // 大屏:头像点击打开订阅弹窗
cursor="pointer" <>
onClick={() => setIsSubscriptionModalOpen(true)}
>
{/* 会员图标徽章 - 使用独立的 CrownTooltip 组件 */}
<CrownTooltip subscriptionInfo={subscriptionInfo} />
{/* 头像 - 带会员等级边框和详细信息 Tooltip */}
<Tooltip <Tooltip
label={<TooltipContent subscriptionInfo={subscriptionInfo} />} label={<TooltipContent subscriptionInfo={subscriptionInfo} />}
placement="bottom" placement="bottom"
@@ -957,6 +1066,12 @@ export default function HomeNavbar() {
boxShadow="lg" boxShadow="lg"
p={3} p={3}
> >
<Box
position="relative"
cursor="pointer"
onClick={() => setIsSubscriptionModalOpen(true)}
>
<CrownIcon subscriptionInfo={subscriptionInfo} />
<Avatar <Avatar
size="sm" size="sm"
name={getDisplayName()} name={getDisplayName()}
@@ -975,10 +1090,61 @@ export default function HomeNavbar() {
}} }}
transition="all 0.2s" transition="all 0.2s"
/> />
</Box>
</Tooltip> </Tooltip>
{isSubscriptionModalOpen && (
<SubscriptionModal
isOpen={isSubscriptionModalOpen}
onClose={() => setIsSubscriptionModalOpen(false)}
subscriptionInfo={subscriptionInfo}
/>
)}
</>
) : (
// 中屏:头像作为下拉菜单,包含所有功能
<Menu>
<MenuButton>
<Box position="relative">
<CrownIcon subscriptionInfo={subscriptionInfo} />
<Avatar
size="sm"
name={getDisplayName()}
src={user.avatar_url}
bg="blue.500"
border={subscriptionInfo.type !== 'free' ? '2.5px solid' : 'none'}
borderColor={
subscriptionInfo.type === 'max' ? '#667eea' :
subscriptionInfo.type === 'pro' ? '#667eea' : 'transparent'
}
_hover={{ transform: 'scale(1.05)' }}
transition="all 0.2s"
/>
</Box>
</MenuButton>
<MenuList minW="320px">
{/* 用户信息区 */}
<Box px={3} py={2} borderBottom="1px" borderColor={useColorModeValue('gray.200', 'gray.600')}>
<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> </Box>
{/* 订阅管理弹窗 - 只在打开时渲染 */} {/* 订阅管理 */}
<MenuItem icon={<FaCrown />} onClick={() => setIsSubscriptionModalOpen(true)}>
<Flex justify="space-between" align="center" w="100%">
<Text>订阅管理</Text>
<Badge colorScheme={subscriptionInfo.type === 'free' ? 'gray' : 'purple'}>
{subscriptionInfo.type === 'max' ? 'MAX' :
subscriptionInfo.type === 'pro' ? 'PRO' : '免费版'}
</Badge>
</Flex>
</MenuItem>
{isSubscriptionModalOpen && ( {isSubscriptionModalOpen && (
<SubscriptionModal <SubscriptionModal
isOpen={isSubscriptionModalOpen} isOpen={isSubscriptionModalOpen}
@@ -987,7 +1153,53 @@ export default function HomeNavbar() {
/> />
)} )}
{/* 个人中心下拉菜单 */} <MenuDivider />
{/* 自选股 */}
<MenuItem icon={<FiStar />} onClick={() => navigate('/home/center')}>
<Flex justify="space-between" align="center" w="100%">
<Text>我的自选股</Text>
{watchlistQuotes && watchlistQuotes.length > 0 && (
<Badge>{watchlistQuotes.length}</Badge>
)}
</Flex>
</MenuItem>
{/* 自选事件 */}
<MenuItem icon={<FiCalendar />} onClick={() => navigate('/home/center')}>
<Flex justify="space-between" align="center" w="100%">
<Text>我的自选事件</Text>
{followingEvents && followingEvents.length > 0 && (
<Badge>{followingEvents.length}</Badge>
)}
</Flex>
</MenuItem>
<MenuDivider />
{/* 个人中心 */}
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
个人中心
</MenuItem>
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
个人资料
</MenuItem>
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
账户设置
</MenuItem>
<MenuDivider />
{/* 退出登录 */}
<MenuItem icon={<FiLogOut />} onClick={handleLogout} color="red.500">
退出登录
</MenuItem>
</MenuList>
</Menu>
)}
{/* 个人中心下拉菜单 - 仅大屏显示 */}
{isDesktop && (
<Menu> <Menu>
<MenuButton <MenuButton
as={Button} as={Button}
@@ -1033,6 +1245,7 @@ export default function HomeNavbar() {
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
)}
</HStack> </HStack>
) : ( ) : (
// 未登录状态 - 单一按钮 // 未登录状态 - 单一按钮

View File

@@ -0,0 +1,137 @@
// src/components/Subscription/CrownTooltip.js
import React from 'react';
import {
Box,
VStack,
HStack,
Text,
Divider,
useColorModeValue
} from '@chakra-ui/react';
import PropTypes from 'prop-types';
/**
* Tooltip 内容组件 - 显示详细的会员信息
* 导出此组件供头像也使用相同的 Tooltip 内容
*/
export const TooltipContent = ({ subscriptionInfo }) => {
const tooltipText = useColorModeValue('gray.700', 'gray.100');
const dividerColor = useColorModeValue('gray.200', 'gray.600');
const { type, days_left, is_active } = subscriptionInfo;
// 基础版用户
if (type === 'free') {
return (
<VStack spacing={2} align="stretch" minW="200px">
<Text fontSize="md" fontWeight="bold" color={tooltipText}>
基础版用户
</Text>
<Divider borderColor={dividerColor} />
<Text fontSize="sm" color={tooltipText} opacity={0.8}>
解锁更多高级功能
</Text>
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
点击头像升级会员
</Text>
</VStack>
);
}
// 付费用户
const isExpired = !is_active;
const isUrgent = days_left < 7;
const isWarning = days_left < 30;
return (
<VStack spacing={2} align="stretch" minW="220px">
<HStack justify="space-between">
<Text fontSize="md" fontWeight="bold" color={tooltipText}>
{type === 'pro' ? '💎 Pro 会员' : '👑 Max 会员'}
</Text>
{isExpired && <Text fontSize="xs" color="red.500">已过期</Text>}
</HStack>
<Divider borderColor={dividerColor} />
{/* 状态信息 */}
{isExpired ? (
<HStack spacing={2}>
<Text fontSize="sm" color="red.500"></Text>
<Text fontSize="sm" color={tooltipText}>
会员已过期续费恢复权益
</Text>
</HStack>
) : (
<VStack spacing={1} align="stretch">
<HStack spacing={2}>
<Text fontSize="sm">
{isUrgent ? '⚠️' : isWarning ? '⏰' : '📅'}
</Text>
<Text fontSize="sm" color={tooltipText}>
{isUrgent && <Text as="span" color="red.500" fontWeight="600">紧急! </Text>}
还有 <Text as="span" fontWeight="600" color={isUrgent ? 'red.500' : isWarning ? 'orange.500' : tooltipText}>{days_left}</Text>
</Text>
</HStack>
<Text fontSize="xs" color={tooltipText} opacity={0.7} pl={6}>
享受全部高级功能
</Text>
</VStack>
)}
{/* 操作提示 */}
<Text fontSize="xs" color={tooltipText} opacity={0.6} textAlign="center" mt={1}>
{isExpired || isUrgent ? '点击头像立即续费' : '点击头像管理订阅'}
</Text>
</VStack>
);
};
/**
* 皇冠图标组件 - 显示在头像左上角的会员标识(纯图标,无 Tooltip
* Tooltip 由外层统一包裹
*
* @param {Object} subscriptionInfo - 订阅信息
* @param {string} subscriptionInfo.type - 订阅类型: 'free' | 'pro' | 'max'
*/
export function CrownIcon({ subscriptionInfo }) {
// 基础版用户不显示皇冠
if (subscriptionInfo.type === 'free') {
return null;
}
return (
<Box
position="absolute"
top="-8px"
left="-8px"
zIndex={2}
display="flex"
alignItems="center"
justifyContent="center"
w="24px"
h="24px"
_hover={{
transform: 'scale(1.2)',
}}
transition="all 0.3s ease"
>
<Text fontSize="20px" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.3))">
{subscriptionInfo.type === 'max' ? '👑' : '💎'}
</Text>
</Box>
);
}
CrownIcon.propTypes = {
subscriptionInfo: PropTypes.shape({
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
}).isRequired,
};
TooltipContent.propTypes = {
subscriptionInfo: PropTypes.shape({
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
days_left: PropTypes.number,
is_active: PropTypes.bool,
}).isRequired,
};