380 lines
11 KiB
TypeScript
380 lines
11 KiB
TypeScript
/**
|
|
* 股票社区主页面 - HeroUI 深色风格
|
|
* Discord 风格三栏布局:频道列表 | 消息区域 | 右侧面板
|
|
* 支持 Tab 切换:社区频道 / 预测市场
|
|
*/
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
Box,
|
|
Flex,
|
|
IconButton,
|
|
Drawer,
|
|
DrawerOverlay,
|
|
DrawerContent,
|
|
useDisclosure,
|
|
useBreakpointValue,
|
|
Text,
|
|
HStack,
|
|
Icon,
|
|
Tooltip,
|
|
Tabs,
|
|
TabList,
|
|
Tab,
|
|
TabPanels,
|
|
TabPanel,
|
|
Badge,
|
|
} from '@chakra-ui/react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Menu, Users, Hash, Settings, Bell, Search, MessageSquare, Zap } from 'lucide-react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
|
|
import ChannelSidebar from './components/ChannelSidebar';
|
|
import MessageArea from './components/MessageArea';
|
|
import RightPanel from './components/RightPanel';
|
|
import PredictionMarket from './components/PredictionMarket';
|
|
import { useCommunitySocket } from './hooks/useCommunitySocket';
|
|
import { Channel } from './types';
|
|
import { GLASS_BLUR, GLASS_BG, GLASS_BORDER, GLASS_SHADOW } from '@/constants/glassConfig';
|
|
|
|
// 动画配置
|
|
const slideVariants = {
|
|
hidden: { opacity: 0, x: -20 },
|
|
visible: { opacity: 1, x: 0, transition: { duration: 0.3, ease: 'easeOut' as const } },
|
|
exit: { opacity: 0, x: -20, transition: { duration: 0.2 } },
|
|
};
|
|
|
|
const StockCommunity: React.FC = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [activeChannel, setActiveChannel] = useState<Channel | null>(null);
|
|
const [rightPanelContent, setRightPanelContent] = useState<'members' | 'thread' | 'info'>('members');
|
|
const [activeTab, setActiveTab] = useState(0); // 0: 社区频道, 1: 预测市场
|
|
|
|
// 移动端抽屉
|
|
const { isOpen: isLeftOpen, onOpen: onLeftOpen, onClose: onLeftClose } = useDisclosure();
|
|
const { isOpen: isRightOpen, onOpen: onRightOpen, onClose: onRightClose } = useDisclosure();
|
|
|
|
// 响应式布局
|
|
const isMobile = useBreakpointValue({ base: true, lg: false });
|
|
const showLeftSidebar = useBreakpointValue({ base: false, lg: true });
|
|
const showRightPanel = useBreakpointValue({ base: false, xl: true });
|
|
|
|
// WebSocket 连接
|
|
const { isConnected, subscribe, unsubscribe } = useCommunitySocket();
|
|
|
|
// 从 URL 参数获取频道 ID
|
|
useEffect(() => {
|
|
const channelId = searchParams.get('channel');
|
|
if (channelId && !activeChannel) {
|
|
// 将在 ChannelSidebar 加载频道后设置
|
|
}
|
|
}, [searchParams, activeChannel]);
|
|
|
|
// 切换频道
|
|
const handleChannelSelect = useCallback((channel: Channel) => {
|
|
// 取消订阅旧频道
|
|
if (activeChannel) {
|
|
unsubscribe(activeChannel.id);
|
|
}
|
|
|
|
// 订阅新频道
|
|
subscribe(channel.id);
|
|
setActiveChannel(channel);
|
|
|
|
// 更新 URL
|
|
setSearchParams({ channel: channel.id });
|
|
|
|
// 移动端关闭抽屉
|
|
if (isMobile) {
|
|
onLeftClose();
|
|
}
|
|
}, [activeChannel, subscribe, unsubscribe, setSearchParams, isMobile, onLeftClose]);
|
|
|
|
// 渲染频道侧边栏
|
|
const renderChannelSidebar = () => (
|
|
<ChannelSidebar
|
|
activeChannelId={activeChannel?.id}
|
|
onChannelSelect={handleChannelSelect}
|
|
initialChannelId={searchParams.get('channel')}
|
|
/>
|
|
);
|
|
|
|
// 渲染右侧面板
|
|
const renderRightPanel = () => (
|
|
<RightPanel
|
|
channel={activeChannel}
|
|
contentType={rightPanelContent}
|
|
onContentTypeChange={setRightPanelContent}
|
|
/>
|
|
);
|
|
|
|
// 渲染社区频道视图(原有的 Discord 风格三栏布局)
|
|
const renderCommunityView = () => (
|
|
<Flex
|
|
h="full"
|
|
overflow="hidden"
|
|
position="relative"
|
|
>
|
|
{/* 左侧频道列表 - 桌面端 */}
|
|
<AnimatePresence>
|
|
{showLeftSidebar && (
|
|
<motion.div
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="exit"
|
|
variants={slideVariants}
|
|
style={{ display: 'flex', height: '100%' }}
|
|
>
|
|
<Box
|
|
w="280px"
|
|
minW="280px"
|
|
h="full"
|
|
bg="rgba(17, 24, 39, 0.8)"
|
|
backdropFilter={`${GLASS_BLUR.lg} saturate(180%)`}
|
|
borderRight="1px solid"
|
|
borderColor="rgba(255, 255, 255, 0.08)"
|
|
boxShadow="4px 0 24px rgba(0, 0, 0, 0.3)"
|
|
overflow="hidden"
|
|
display="flex"
|
|
flexDirection="column"
|
|
>
|
|
{renderChannelSidebar()}
|
|
</Box>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 左侧频道列表 - 移动端抽屉 */}
|
|
<Drawer isOpen={isLeftOpen} placement="left" onClose={onLeftClose}>
|
|
<DrawerOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
|
<DrawerContent
|
|
maxW="300px"
|
|
bg="rgba(17, 24, 39, 0.98)"
|
|
backdropFilter={GLASS_BLUR.lg}
|
|
>
|
|
{renderChannelSidebar()}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
|
|
{/* 中间消息区域 */}
|
|
<Box
|
|
flex={1}
|
|
h="full"
|
|
overflow="hidden"
|
|
bg="rgba(15, 23, 42, 0.6)"
|
|
position="relative"
|
|
>
|
|
<MessageArea
|
|
channel={activeChannel}
|
|
onOpenRightPanel={onRightOpen}
|
|
onRightPanelContentChange={setRightPanelContent}
|
|
/>
|
|
</Box>
|
|
|
|
{/* 右侧面板 - 桌面端 */}
|
|
<AnimatePresence>
|
|
{showRightPanel && activeChannel && (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0, transition: { duration: 0.3 } }}
|
|
exit={{ opacity: 0, x: 20, transition: { duration: 0.2 } }}
|
|
style={{ display: 'flex', height: '100%' }}
|
|
>
|
|
<Box
|
|
w="300px"
|
|
minW="300px"
|
|
h="full"
|
|
bg="rgba(17, 24, 39, 0.8)"
|
|
backdropFilter={`${GLASS_BLUR.lg} saturate(180%)`}
|
|
borderLeft="1px solid"
|
|
borderColor="rgba(255, 255, 255, 0.08)"
|
|
boxShadow="-4px 0 24px rgba(0, 0, 0, 0.3)"
|
|
overflow="hidden"
|
|
>
|
|
{renderRightPanel()}
|
|
</Box>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* 右侧面板 - 移动端抽屉 */}
|
|
<Drawer isOpen={isRightOpen} placement="right" onClose={onRightClose}>
|
|
<DrawerOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
|
<DrawerContent
|
|
maxW="320px"
|
|
bg="rgba(17, 24, 39, 0.98)"
|
|
backdropFilter={GLASS_BLUR.lg}
|
|
>
|
|
{renderRightPanel()}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
</Flex>
|
|
);
|
|
|
|
return (
|
|
<Box
|
|
h="calc(100vh - 60px)"
|
|
bg="gray.900"
|
|
overflow="hidden"
|
|
position="relative"
|
|
>
|
|
{/* 背景装饰 */}
|
|
<Box
|
|
position="absolute"
|
|
top="-10%"
|
|
left="-5%"
|
|
w="400px"
|
|
h="400px"
|
|
bg={activeTab === 0 ? 'purple.500' : 'yellow.500'}
|
|
filter="blur(150px)"
|
|
opacity="0.08"
|
|
borderRadius="full"
|
|
pointerEvents="none"
|
|
transition="background 0.5s ease"
|
|
/>
|
|
<Box
|
|
position="absolute"
|
|
bottom="-10%"
|
|
right="-5%"
|
|
w="350px"
|
|
h="350px"
|
|
bg={activeTab === 0 ? 'blue.500' : 'orange.500'}
|
|
filter="blur(150px)"
|
|
opacity="0.06"
|
|
borderRadius="full"
|
|
pointerEvents="none"
|
|
transition="background 0.5s ease"
|
|
/>
|
|
|
|
{/* Tabs 容器 */}
|
|
<Tabs
|
|
index={activeTab}
|
|
onChange={(index) => setActiveTab(index)}
|
|
isLazy
|
|
h="full"
|
|
display="flex"
|
|
flexDirection="column"
|
|
>
|
|
{/* Tab 切换栏 */}
|
|
<Box
|
|
bg="rgba(17, 24, 39, 0.95)"
|
|
backdropFilter={GLASS_BLUR.lg}
|
|
borderBottom="1px solid"
|
|
borderColor="rgba(255, 255, 255, 0.08)"
|
|
px={4}
|
|
flexShrink={0}
|
|
>
|
|
<Flex align="center" justify="space-between">
|
|
{/* 移动端菜单按钮 */}
|
|
{isMobile && activeTab === 0 && (
|
|
<IconButton
|
|
aria-label="打开频道列表"
|
|
icon={<Menu className="w-5 h-5" />}
|
|
variant="ghost"
|
|
color="gray.300"
|
|
size="sm"
|
|
mr={2}
|
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
|
onClick={onLeftOpen}
|
|
/>
|
|
)}
|
|
|
|
<TabList
|
|
border="none"
|
|
gap={2}
|
|
py={2}
|
|
flex={1}
|
|
>
|
|
<Tab
|
|
px={4}
|
|
py={2}
|
|
borderRadius="lg"
|
|
fontWeight="semibold"
|
|
fontSize="sm"
|
|
color="gray.400"
|
|
bg="transparent"
|
|
border="none"
|
|
_selected={{
|
|
color: 'white',
|
|
bg: 'linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(59, 130, 246, 0.3))',
|
|
boxShadow: '0 0 20px rgba(139, 92, 246, 0.2)',
|
|
}}
|
|
_hover={{
|
|
color: 'white',
|
|
bg: 'whiteAlpha.100',
|
|
}}
|
|
>
|
|
<HStack spacing={2}>
|
|
<MessageSquare className="w-4 h-4" />
|
|
<Text>社区频道</Text>
|
|
</HStack>
|
|
</Tab>
|
|
<Tab
|
|
px={4}
|
|
py={2}
|
|
borderRadius="lg"
|
|
fontWeight="semibold"
|
|
fontSize="sm"
|
|
color="gray.400"
|
|
bg="transparent"
|
|
border="none"
|
|
_selected={{
|
|
color: 'white',
|
|
bg: 'linear-gradient(135deg, rgba(251, 191, 36, 0.3), rgba(245, 158, 11, 0.3))',
|
|
boxShadow: '0 0 20px rgba(251, 191, 36, 0.2)',
|
|
}}
|
|
_hover={{
|
|
color: 'white',
|
|
bg: 'whiteAlpha.100',
|
|
}}
|
|
>
|
|
<HStack spacing={2}>
|
|
<Zap className="w-4 h-4" />
|
|
<Text>预测市场</Text>
|
|
<Badge
|
|
bg="linear-gradient(135deg, rgba(239, 68, 68, 0.4), rgba(220, 38, 38, 0.4))"
|
|
color="red.200"
|
|
fontSize="2xs"
|
|
px={1.5}
|
|
borderRadius="full"
|
|
>
|
|
BETA
|
|
</Badge>
|
|
</HStack>
|
|
</Tab>
|
|
</TabList>
|
|
|
|
{/* 移动端右侧按钮 */}
|
|
{isMobile && activeTab === 0 && (
|
|
<IconButton
|
|
aria-label="打开详情面板"
|
|
icon={<Users className="w-5 h-5" />}
|
|
variant="ghost"
|
|
color="gray.300"
|
|
size="sm"
|
|
ml={2}
|
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
|
onClick={onRightOpen}
|
|
/>
|
|
)}
|
|
</Flex>
|
|
</Box>
|
|
|
|
{/* Tab 内容区域 */}
|
|
<TabPanels flex={1} overflow="hidden">
|
|
{/* 社区频道 */}
|
|
<TabPanel p={0} h="full">
|
|
{renderCommunityView()}
|
|
</TabPanel>
|
|
|
|
{/* 预测市场 */}
|
|
<TabPanel p={0} h="full">
|
|
<PredictionMarket />
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default StockCommunity;
|