feat(GlobalSidebar): 收起状态添加 Popover 悬浮弹窗

- 收起状态点击图标显示悬浮弹窗,无需展开侧边栏
- 添加关注股票、关注事件、热门板块三个 Popover 面板
- 展开状态添加独立标题栏 [>] 工具栏
- 移除收起按钮的 Tooltip 提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-23 14:15:20 +08:00
parent c325d51316
commit 92e6fb254b

View File

@@ -2,6 +2,7 @@
* GlobalSidebar - 全局右侧工具栏 * GlobalSidebar - 全局右侧工具栏
* *
* 可收起/展开的侧边栏,包含关注股票和事件动态 * 可收起/展开的侧边栏,包含关注股票和事件动态
* 收起时点击图标显示悬浮弹窗
*/ */
import React from 'react'; import React from 'react';
@@ -10,98 +11,231 @@ import {
VStack, VStack,
Icon, Icon,
IconButton, IconButton,
Tooltip,
Badge, Badge,
Spinner, Spinner,
Center, Center,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
PopoverHeader,
PopoverCloseButton,
Text,
HStack,
Portal,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChevronLeft, ChevronRight, BarChart2, Star } from 'lucide-react'; import { ChevronLeft, ChevronRight, BarChart2, Star, TrendingUp } from 'lucide-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useGlobalSidebar } from '@/contexts/GlobalSidebarContext'; import { useGlobalSidebar } from '@/contexts/GlobalSidebarContext';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { getEventDetailUrl } from '@/utils/idEncoder'; import { getEventDetailUrl } from '@/utils/idEncoder';
import WatchSidebar from '@views/Profile/components/WatchSidebar'; import WatchSidebar from '@views/Profile/components/WatchSidebar';
import { WatchlistPanel, FollowingEventsPanel } from '@views/Profile/components/WatchSidebar/components';
import HotSectorsRanking from '@views/Profile/components/MarketDashboard/components/atoms/HotSectorsRanking';
/** /**
* 收起状态下的图标菜单 * 收起状态下的图标菜单(带悬浮弹窗)
*/ */
const CollapsedMenu = ({ watchlist, followingEvents, onToggle }) => { const CollapsedMenu = ({
watchlist,
realtimeQuotes,
followingEvents,
eventComments,
onToggle,
onStockClick,
onEventClick,
onCommentClick,
onAddStock,
onAddEvent,
onUnwatch,
onUnfollow,
}) => {
return ( return (
<VStack spacing={4} py={4} align="center"> <VStack spacing={4} py={4} align="center">
{/* 展开按钮 */} {/* 展开按钮 */}
<Tooltip label="展开工具栏" placement="left"> <HStack spacing={1} w="100%" justify="center" cursor="pointer" onClick={onToggle} _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} py={1} borderRadius="md">
<IconButton <Icon as={ChevronLeft} boxSize={4} color="rgba(255, 255, 255, 0.6)" />
icon={<Icon as={ChevronLeft} />} <Text fontSize="10px" color="rgba(255, 255, 255, 0.5)">
size="sm" 展开
variant="ghost" </Text>
color="rgba(255, 255, 255, 0.6)" </HStack>
_hover={{ color: 'rgba(212, 175, 55, 0.9)', bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={onToggle}
aria-label="展开工具栏"
/>
</Tooltip>
{/* 关注股票图标 */} {/* 关注股票 - 悬浮弹窗 */}
<Tooltip label={`关注股票 (${watchlist.length})`} placement="left"> <Popover placement="left-start" trigger="click" isLazy>
<Box <PopoverTrigger>
position="relative" <VStack
cursor="pointer" spacing={1}
p={2} align="center"
borderRadius="md" cursor="pointer"
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} p={2}
onClick={onToggle} borderRadius="md"
> _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
<Icon as={BarChart2} boxSize={5} color="rgba(59, 130, 246, 0.9)" /> position="relative"
{watchlist.length > 0 && ( >
<Badge <Box position="relative">
position="absolute" <Icon as={BarChart2} boxSize={5} color="rgba(59, 130, 246, 0.9)" />
top="-2px" {watchlist.length > 0 && (
right="-2px" <Badge
colorScheme="red" position="absolute"
fontSize="9px" top="-4px"
minW="16px" right="-8px"
h="16px" colorScheme="red"
borderRadius="full" fontSize="9px"
display="flex" minW="16px"
alignItems="center" h="16px"
justifyContent="center" borderRadius="full"
display="flex"
alignItems="center"
justifyContent="center"
>
{watchlist.length > 99 ? '99+' : watchlist.length}
</Badge>
)}
</Box>
<Text fontSize="10px" color="rgba(255, 255, 255, 0.6)" whiteSpace="nowrap">
关注股票
</Text>
</VStack>
</PopoverTrigger>
<Portal>
<PopoverContent
w="300px"
bg="rgba(26, 32, 44, 0.95)"
borderColor="rgba(255, 255, 255, 0.1)"
boxShadow="0 8px 32px rgba(0, 0, 0, 0.4)"
_focus={{ outline: 'none' }}
>
<PopoverHeader
borderBottomColor="rgba(255, 255, 255, 0.1)"
py={2}
px={3}
> >
{watchlist.length > 99 ? '99+' : watchlist.length} <HStack spacing={2}>
</Badge> <Icon as={BarChart2} boxSize={4} color="rgba(59, 130, 246, 0.9)" />
)} <Text fontSize="sm" fontWeight="bold" color="rgba(255, 255, 255, 0.9)">
</Box> 关注股票 ({watchlist.length})
</Tooltip> </Text>
</HStack>
</PopoverHeader>
<PopoverCloseButton color="rgba(255, 255, 255, 0.5)" />
<PopoverBody p={2}>
<WatchlistPanel
watchlist={watchlist}
realtimeQuotes={realtimeQuotes}
onStockClick={onStockClick}
onAddStock={onAddStock}
onUnwatch={onUnwatch}
/>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
{/* 事件动态图标 */} {/* 事件动态 - 悬浮弹窗 */}
<Tooltip label={`事件动态 (${followingEvents.length})`} placement="left"> <Popover placement="left-start" trigger="click" isLazy>
<Box <PopoverTrigger>
position="relative" <VStack
cursor="pointer" spacing={1}
p={2} align="center"
borderRadius="md" cursor="pointer"
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }} p={2}
onClick={onToggle} borderRadius="md"
> _hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
<Icon as={Star} boxSize={5} color="rgba(234, 179, 8, 0.9)" /> position="relative"
{followingEvents.length > 0 && ( >
<Badge <Box position="relative">
position="absolute" <Icon as={Star} boxSize={5} color="rgba(234, 179, 8, 0.9)" />
top="-2px" {followingEvents.length > 0 && (
right="-2px" <Badge
colorScheme="yellow" position="absolute"
fontSize="9px" top="-4px"
minW="16px" right="-8px"
h="16px" colorScheme="yellow"
borderRadius="full" fontSize="9px"
display="flex" minW="16px"
alignItems="center" h="16px"
justifyContent="center" borderRadius="full"
display="flex"
alignItems="center"
justifyContent="center"
>
{followingEvents.length > 99 ? '99+' : followingEvents.length}
</Badge>
)}
</Box>
<Text fontSize="10px" color="rgba(255, 255, 255, 0.6)" whiteSpace="nowrap">
关注事件
</Text>
</VStack>
</PopoverTrigger>
<Portal>
<PopoverContent
w="300px"
bg="rgba(26, 32, 44, 0.95)"
borderColor="rgba(255, 255, 255, 0.1)"
boxShadow="0 8px 32px rgba(0, 0, 0, 0.4)"
_focus={{ outline: 'none' }}
>
<PopoverHeader
borderBottomColor="rgba(255, 255, 255, 0.1)"
py={2}
px={3}
> >
{followingEvents.length > 99 ? '99+' : followingEvents.length} <HStack spacing={2}>
</Badge> <Icon as={Star} boxSize={4} color="rgba(234, 179, 8, 0.9)" />
)} <Text fontSize="sm" fontWeight="bold" color="rgba(255, 255, 255, 0.9)">
</Box> 事件动态
</Tooltip> </Text>
</HStack>
</PopoverHeader>
<PopoverCloseButton color="rgba(255, 255, 255, 0.5)" />
<PopoverBody p={2}>
<FollowingEventsPanel
events={followingEvents}
eventComments={eventComments}
onEventClick={onEventClick}
onCommentClick={onCommentClick}
onAddEvent={onAddEvent}
onUnfollow={onUnfollow}
/>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
{/* 热门板块 - 悬浮弹窗 */}
<Popover placement="left-start" trigger="click" isLazy>
<PopoverTrigger>
<VStack
spacing={1}
align="center"
cursor="pointer"
p={2}
borderRadius="md"
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
position="relative"
>
<Icon as={TrendingUp} boxSize={5} color="rgba(34, 197, 94, 0.9)" />
<Text fontSize="10px" color="rgba(255, 255, 255, 0.6)" whiteSpace="nowrap">
热门板块
</Text>
</VStack>
</PopoverTrigger>
<Portal>
<PopoverContent
w="280px"
bg="rgba(26, 32, 44, 0.95)"
borderColor="rgba(255, 255, 255, 0.1)"
boxShadow="0 8px 32px rgba(0, 0, 0, 0.4)"
_focus={{ outline: 'none' }}
>
<PopoverCloseButton color="rgba(255, 255, 255, 0.5)" />
<PopoverBody p={2}>
<HotSectorsRanking title="热门板块" />
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
</VStack> </VStack>
); );
}; };
@@ -132,7 +266,7 @@ const GlobalSidebar = () => {
return ( return (
<Box <Box
w={isOpen ? '300px' : '48px'} w={isOpen ? '300px' : '72px'}
flexShrink={0} flexShrink={0}
transition="width 0.2s ease-in-out" transition="width 0.2s ease-in-out"
display={{ base: 'none', md: 'block' }} display={{ base: 'none', md: 'block' }}
@@ -149,24 +283,30 @@ const GlobalSidebar = () => {
{isOpen ? ( {isOpen ? (
/* 展开状态 */ /* 展开状态 */
<Box h="100%" overflowY="auto" position="relative"> <Box h="100%" display="flex" flexDirection="column">
{/* 收起按钮 */} {/* 标题栏 - 收起按钮 + 标题 */}
<Box position="absolute" top={2} left={2} zIndex={2}> <HStack
<Tooltip label="收起工具栏" placement="right"> px={3}
<IconButton py={2}
icon={<Icon as={ChevronRight} />} borderBottom="1px solid rgba(255, 255, 255, 0.05)"
size="xs" flexShrink={0}
variant="ghost" >
color="rgba(255, 255, 255, 0.4)" <IconButton
_hover={{ color: 'rgba(212, 175, 55, 0.9)', bg: 'rgba(255, 255, 255, 0.05)' }} icon={<Icon as={ChevronRight} />}
onClick={toggle} size="xs"
aria-label="收起工具栏" variant="ghost"
/> color="rgba(255, 255, 255, 0.5)"
</Tooltip> _hover={{ color: 'rgba(212, 175, 55, 0.9)', bg: 'rgba(255, 255, 255, 0.05)' }}
</Box> onClick={toggle}
aria-label="收起工具栏"
/>
<Text fontSize="sm" fontWeight="medium" color="rgba(255, 255, 255, 0.7)">
工具栏
</Text>
</HStack>
{/* WatchSidebar 内容 */} {/* WatchSidebar 内容 */}
<Box pt={2} px={2}> <Box flex="1" overflowY="auto" pt={2} px={2}>
<WatchSidebar <WatchSidebar
watchlist={watchlist} watchlist={watchlist}
realtimeQuotes={realtimeQuotes} realtimeQuotes={realtimeQuotes}
@@ -183,11 +323,20 @@ const GlobalSidebar = () => {
</Box> </Box>
</Box> </Box>
) : ( ) : (
/* 收起状态 */ /* 收起状态 - 点击图标显示悬浮弹窗 */
<CollapsedMenu <CollapsedMenu
watchlist={watchlist} watchlist={watchlist}
realtimeQuotes={realtimeQuotes}
followingEvents={followingEvents} followingEvents={followingEvents}
eventComments={eventComments}
onToggle={toggle} onToggle={toggle}
onStockClick={(stock) => navigate(`/company/${stock.stock_code}`)}
onEventClick={(event) => navigate(getEventDetailUrl(event.id))}
onCommentClick={(comment) => navigate(getEventDetailUrl(comment.event_id))}
onAddStock={() => navigate('/stocks')}
onAddEvent={() => navigate('/community')}
onUnwatch={unwatchStock}
onUnfollow={unfollowEvent}
/> />
)} )}
</Box> </Box>