/** * 灵活屏组件 * 用户可自定义添加关注的指数/个股,实时显示行情 * * 功能: * 1. 添加/删除自选证券 * 2. 显示实时行情(通过 WebSocket) * 3. 显示分时走势(结合 ClickHouse 历史数据) * 4. 显示五档盘口(上交所5档,深交所10档) * 5. 本地存储自选列表 */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Box, Card, CardBody, VStack, HStack, Heading, Text, Input, InputGroup, InputLeftElement, InputRightElement, IconButton, SimpleGrid, Flex, Spacer, Icon, useColorModeValue, useToast, Badge, Tooltip, Collapse, List, ListItem, Spinner, Center, Menu, MenuButton, MenuList, MenuItem, Tag, TagLabel, } from '@chakra-ui/react'; import { SearchIcon, CloseIcon, AddIcon, ChevronDownIcon, ChevronUpIcon, SettingsIcon, } from '@chakra-ui/icons'; import { FaDesktop, FaTrash, FaSync, FaWifi, FaExclamationCircle, } from 'react-icons/fa'; import { useRealtimeQuote } from './hooks'; import QuoteTile from './components/QuoteTile'; import { logger } from '@utils/logger'; import type { WatchlistItem, ConnectionStatus } from './types'; // 本地存储 key const STORAGE_KEY = 'flexscreen_watchlist'; // 默认自选列表 const DEFAULT_WATCHLIST: WatchlistItem[] = [ { code: '000001', name: '上证指数', isIndex: true }, { code: '399001', name: '深证成指', isIndex: true }, { code: '399006', name: '创业板指', isIndex: true }, ]; // 热门推荐 const HOT_RECOMMENDATIONS: WatchlistItem[] = [ { code: '000001', name: '上证指数', isIndex: true }, { code: '399001', name: '深证成指', isIndex: true }, { code: '399006', name: '创业板指', isIndex: true }, { code: '399300', name: '沪深300', isIndex: true }, { code: '600519', name: '贵州茅台', isIndex: false }, { code: '000858', name: '五粮液', isIndex: false }, { code: '300750', name: '宁德时代', isIndex: false }, { code: '002594', name: '比亚迪', isIndex: false }, ]; /** 搜索结果项 */ interface SearchResultItem { stock_code: string; stock_name: string; isIndex?: boolean; code?: string; name?: string; } /** 搜索 API 响应 */ interface SearchApiResponse { success: boolean; data?: SearchResultItem[]; } /** 连接状态信息 */ interface ConnectionStatusInfo { color: string; text: string; } /** * FlexScreen 组件 */ const FlexScreen: React.FC = () => { const toast = useToast(); // 自选列表 const [watchlist, setWatchlist] = useState([]); // 搜索状态 const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showResults, setShowResults] = useState(false); // 面板状态 const [isCollapsed, setIsCollapsed] = useState(false); // 颜色主题 const cardBg = useColorModeValue('white', '#1a1a1a'); const borderColor = useColorModeValue('gray.200', '#333'); const textColor = useColorModeValue('gray.800', 'white'); const subTextColor = useColorModeValue('gray.600', 'gray.400'); const searchBg = useColorModeValue('gray.50', '#2a2a2a'); const hoverBg = useColorModeValue('gray.100', '#333'); // 获取订阅的证券代码列表 const subscribedCodes = useMemo(() => { return watchlist.map(item => item.code); }, [watchlist]); // WebSocket 实时行情 const { quotes, connected } = useRealtimeQuote(subscribedCodes); // 从本地存储加载自选列表 useEffect(() => { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved) as WatchlistItem[]; if (Array.isArray(parsed) && parsed.length > 0) { setWatchlist(parsed); return; } } } catch (e) { logger.warn('FlexScreen', '加载自选列表失败', e); } // 使用默认列表 setWatchlist(DEFAULT_WATCHLIST); }, []); // 保存自选列表到本地存储 useEffect(() => { if (watchlist.length > 0) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(watchlist)); } catch (e) { logger.warn('FlexScreen', '保存自选列表失败', e); } } }, [watchlist]); // 搜索证券 const searchSecurities = useCallback(async (query: string): Promise => { if (!query.trim()) { setSearchResults([]); setShowResults(false); return; } setIsSearching(true); try { const response = await fetch(`/api/stocks/search?q=${encodeURIComponent(query)}&limit=10`); const data: SearchApiResponse = await response.json(); if (data.success) { setSearchResults(data.data || []); setShowResults(true); } else { setSearchResults([]); } } catch (e) { logger.error('FlexScreen', '搜索失败', e); setSearchResults([]); } finally { setIsSearching(false); } }, []); // 防抖搜索 useEffect(() => { const timer = setTimeout(() => { searchSecurities(searchQuery); }, 300); return () => clearTimeout(timer); }, [searchQuery, searchSecurities]); // 添加证券 const addSecurity = useCallback( (security: SearchResultItem | WatchlistItem): void => { const code = 'stock_code' in security ? security.stock_code : security.code; const name = 'stock_name' in security ? security.stock_name : security.name; const isIndex = security.isIndex || code.startsWith('000') || code.startsWith('399'); // 检查是否已存在 if (watchlist.some(item => item.code === code)) { toast({ title: '已在自选列表中', status: 'info', duration: 2000, isClosable: true, }); return; } // 添加到列表 setWatchlist(prev => [...prev, { code, name, isIndex }]); toast({ title: `已添加 ${name}`, status: 'success', duration: 2000, isClosable: true, }); // 清空搜索 setSearchQuery(''); setShowResults(false); }, [watchlist, toast] ); // 移除证券 const removeSecurity = useCallback((code: string): void => { setWatchlist(prev => prev.filter(item => item.code !== code)); }, []); // 清空自选列表 const clearWatchlist = useCallback((): void => { setWatchlist([]); localStorage.removeItem(STORAGE_KEY); toast({ title: '已清空自选列表', status: 'info', duration: 2000, isClosable: true, }); }, [toast]); // 重置为默认列表 const resetWatchlist = useCallback((): void => { setWatchlist(DEFAULT_WATCHLIST); toast({ title: '已重置为默认列表', status: 'success', duration: 2000, isClosable: true, }); }, [toast]); // 连接状态指示 const isAnyConnected = connected.SSE || connected.SZSE; const connectionStatus = useMemo((): ConnectionStatusInfo => { if (connected.SSE && connected.SZSE) { return { color: 'green', text: '上交所/深交所 已连接' }; } if (connected.SSE) { return { color: 'yellow', text: '上交所 已连接' }; } if (connected.SZSE) { return { color: 'yellow', text: '深交所 已连接' }; } return { color: 'red', text: '未连接' }; }, [connected]); return ( {/* 头部 */} 灵活屏 {isAnyConnected ? '实时' : '离线'} {/* 操作菜单 */} } size="sm" variant="ghost" aria-label="设置" /> } onClick={resetWatchlist}> 重置为默认 } onClick={clearWatchlist} color="red.500"> 清空列表 {/* 折叠按钮 */} : } size="sm" variant="ghost" onClick={() => setIsCollapsed(!isCollapsed)} aria-label={isCollapsed ? '展开' : '收起'} /> {/* 可折叠内容 */} {/* 搜索框 */} setSearchQuery(e.target.value)} bg={searchBg} borderRadius="lg" _focus={{ borderColor: 'purple.400', boxShadow: '0 0 0 1px var(--chakra-colors-purple-400)', }} /> {searchQuery && ( } variant="ghost" onClick={() => { setSearchQuery(''); setShowResults(false); }} aria-label="清空" /> )} {/* 搜索结果下拉 */} {isSearching ? (
) : searchResults.length > 0 ? ( {searchResults.map((stock, index) => ( addSecurity(stock)} borderBottomWidth={index < searchResults.length - 1 ? '1px' : '0'} borderColor={borderColor} > {stock.stock_name} {stock.stock_code} } size="xs" colorScheme="purple" variant="ghost" aria-label="添加" /> ))} ) : (
未找到相关证券
)}
{/* 快捷添加 */} {watchlist.length === 0 && ( 热门推荐(点击添加) {HOT_RECOMMENDATIONS.map(item => ( addSecurity(item)} > {item.name} ))} )} {/* 自选列表 */} {watchlist.length > 0 ? ( {watchlist.map(item => ( ))} ) : (
自选列表为空,请搜索添加证券
)}
); }; export default FlexScreen;