feat(components): 新增 GlobalSidebar 全局右侧工具栏
- 可收起/展开的侧边栏设计 - 收起状态显示图标菜单(股票数量、事件数量 Badge) - 展开状态复用 WatchSidebar 组件 - 支持未登录状态提示 - 毛玻璃背景 + 金色主题装饰 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
197
src/components/GlobalSidebar/index.js
Normal file
197
src/components/GlobalSidebar/index.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* GlobalSidebar - 全局右侧工具栏
|
||||
*
|
||||
* 可收起/展开的侧边栏,包含关注股票和事件动态
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Spinner,
|
||||
Center,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronLeft, ChevronRight, BarChart2, Star } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGlobalSidebar } from '@/contexts/GlobalSidebarContext';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { getEventDetailUrl } from '@/utils/idEncoder';
|
||||
import WatchSidebar from '@views/Profile/components/WatchSidebar';
|
||||
|
||||
/**
|
||||
* 收起状态下的图标菜单
|
||||
*/
|
||||
const CollapsedMenu = ({ watchlist, followingEvents, onToggle }) => {
|
||||
return (
|
||||
<VStack spacing={4} py={4} align="center">
|
||||
{/* 展开按钮 */}
|
||||
<Tooltip label="展开工具栏" placement="left">
|
||||
<IconButton
|
||||
icon={<Icon as={ChevronLeft} />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="rgba(255, 255, 255, 0.6)"
|
||||
_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">
|
||||
<Box
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon as={BarChart2} boxSize={5} color="rgba(59, 130, 246, 0.9)" />
|
||||
{watchlist.length > 0 && (
|
||||
<Badge
|
||||
position="absolute"
|
||||
top="-2px"
|
||||
right="-2px"
|
||||
colorScheme="red"
|
||||
fontSize="9px"
|
||||
minW="16px"
|
||||
h="16px"
|
||||
borderRadius="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{watchlist.length > 99 ? '99+' : watchlist.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
{/* 事件动态图标 */}
|
||||
<Tooltip label={`事件动态 (${followingEvents.length})`} placement="left">
|
||||
<Box
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon as={Star} boxSize={5} color="rgba(234, 179, 8, 0.9)" />
|
||||
{followingEvents.length > 0 && (
|
||||
<Badge
|
||||
position="absolute"
|
||||
top="-2px"
|
||||
right="-2px"
|
||||
colorScheme="yellow"
|
||||
fontSize="9px"
|
||||
minW="16px"
|
||||
h="16px"
|
||||
borderRadius="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{followingEvents.length > 99 ? '99+' : followingEvents.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* GlobalSidebar 主组件
|
||||
*/
|
||||
const GlobalSidebar = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
toggle,
|
||||
watchlist,
|
||||
realtimeQuotes,
|
||||
followingEvents,
|
||||
eventComments,
|
||||
loading,
|
||||
unwatchStock,
|
||||
unfollowEvent,
|
||||
} = useGlobalSidebar();
|
||||
|
||||
// 未登录时不显示
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
w={isOpen ? '300px' : '48px'}
|
||||
flexShrink={0}
|
||||
transition="width 0.2s ease-in-out"
|
||||
display={{ base: 'none', md: 'block' }}
|
||||
bg="rgba(26, 32, 44, 0.6)"
|
||||
borderLeft="1px solid rgba(255, 255, 255, 0.05)"
|
||||
position="relative"
|
||||
>
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<Center position="absolute" top={4} left={0} right={0} zIndex={1}>
|
||||
<Spinner size="sm" color="rgba(212, 175, 55, 0.6)" />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{isOpen ? (
|
||||
/* 展开状态 */
|
||||
<Box h="100%" overflowY="auto" position="relative">
|
||||
{/* 收起按钮 */}
|
||||
<Box position="absolute" top={2} left={2} zIndex={2}>
|
||||
<Tooltip label="收起工具栏" placement="right">
|
||||
<IconButton
|
||||
icon={<Icon as={ChevronRight} />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="rgba(255, 255, 255, 0.4)"
|
||||
_hover={{ color: 'rgba(212, 175, 55, 0.9)', bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||
onClick={toggle}
|
||||
aria-label="收起工具栏"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* WatchSidebar 内容 */}
|
||||
<Box pt={2} px={2}>
|
||||
<WatchSidebar
|
||||
watchlist={watchlist}
|
||||
realtimeQuotes={realtimeQuotes}
|
||||
followingEvents={followingEvents}
|
||||
eventComments={eventComments}
|
||||
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>
|
||||
) : (
|
||||
/* 收起状态 */
|
||||
<CollapsedMenu
|
||||
watchlist={watchlist}
|
||||
followingEvents={followingEvents}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSidebar;
|
||||
Reference in New Issue
Block a user