refactor(GlobalSidebar): TypeScript 重构与 HotConceptsPanel 模块化
- GlobalSidebar: JS 转换为 TypeScript,添加完整类型定义 - HotConceptsPanel 拆分为模块化目录结构: - types.ts: 类型定义 (Concept, ConceptStock, Props) - styles.ts: 样式常量 + 配置 (COLORS, CONFIG) - utils.ts: 工具函数 (formatChangePercent, getChangeColor) - hooks/useHotConcepts.ts: 数据获取 Hook - components/ConceptCard.tsx: 概念卡片原子组件 (memo 优化) - 性能优化:useMemo 缓存计算,useCallback 缓存事件处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* ConceptCard - 概念卡片原子组件
|
||||
* 展示单个概念的信息:排名、名称、标签、涨跌幅、描述、相关股票
|
||||
*/
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Box, HStack, Text, Badge, Icon, Tag } from '@chakra-ui/react';
|
||||
import { ArrowUp, ArrowDown, Zap } from 'lucide-react';
|
||||
import type { ConceptCardProps } from '../types';
|
||||
import { formatChangePercent, getChangeColor } from '../utils';
|
||||
import { COLORS, CONFIG, conceptCardStyles, linkStyles } from '../styles';
|
||||
|
||||
const ConceptCard: React.FC<ConceptCardProps> = memo(({
|
||||
concept,
|
||||
index,
|
||||
onConceptClick,
|
||||
onStockClick,
|
||||
onViewStocks,
|
||||
}) => {
|
||||
// 缓存计算结果
|
||||
const changeColor = useMemo(() => getChangeColor(concept.change_percent), [concept.change_percent]);
|
||||
const formattedChange = useMemo(() => formatChangePercent(concept.change_percent), [concept.change_percent]);
|
||||
const isUp = concept.change_percent > 0;
|
||||
const isTopRank = index < CONFIG.topRankCount;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={conceptCardStyles}
|
||||
onClick={() => onConceptClick(concept)}
|
||||
>
|
||||
{/* 第一行:排名 + 名称 + 标签 + 涨跌幅 */}
|
||||
<HStack justify="space-between" mb={0.5}>
|
||||
<HStack spacing={1.5} flex={1} minW={0}>
|
||||
<Badge
|
||||
bg={isTopRank ? COLORS.accent : 'gray.600'}
|
||||
color="white"
|
||||
fontSize="10px"
|
||||
px={1.5}
|
||||
py={0}
|
||||
borderRadius="full"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
>
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
color={COLORS.textPrimary}
|
||||
noOfLines={1}
|
||||
flexShrink={0}
|
||||
>
|
||||
{concept.concept_name}
|
||||
</Text>
|
||||
{/* 标签紧跟名称 */}
|
||||
{concept.tags && concept.tags.length > 0 && (
|
||||
<HStack spacing={0.5} flexShrink={1} overflow="hidden">
|
||||
{concept.tags.slice(0, CONFIG.maxTags).map((tag, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
size="sm"
|
||||
bg={COLORS.tagBg}
|
||||
color={COLORS.textPrimary}
|
||||
borderRadius="full"
|
||||
fontSize="9px"
|
||||
px={1.5}
|
||||
py={0}
|
||||
lineHeight="1.3"
|
||||
flexShrink={0}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack spacing={1} flexShrink={0}>
|
||||
<Icon
|
||||
as={isUp ? ArrowUp : ArrowDown}
|
||||
boxSize={3}
|
||||
color={changeColor}
|
||||
/>
|
||||
<Text fontSize="xs" fontWeight="bold" color={changeColor}>
|
||||
{formattedChange}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 第二行:概念描述 */}
|
||||
{concept.description && (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={COLORS.textSecondary}
|
||||
noOfLines={1}
|
||||
mb={0.5}
|
||||
pl={5}
|
||||
>
|
||||
{concept.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 第三行:相关 + 股票链接 + 爆发日期 */}
|
||||
<HStack fontSize="xs" spacing={1} pl={5} flexWrap="wrap">
|
||||
{concept.stocks && concept.stocks.length > 0 && (
|
||||
<>
|
||||
<Text color={COLORS.textMuted}>相关</Text>
|
||||
{concept.stocks.slice(0, CONFIG.maxStocks).map((stock, idx) => (
|
||||
<Text
|
||||
key={idx}
|
||||
sx={linkStyles}
|
||||
onClick={(e) => onStockClick(e, stock.stock_code)}
|
||||
>
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
))}
|
||||
{(concept.stock_count ?? 0) > CONFIG.maxStocks && (
|
||||
<Text
|
||||
sx={linkStyles}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewStocks?.(concept);
|
||||
}}
|
||||
>
|
||||
+{(concept.stock_count ?? 0) - CONFIG.maxStocks}更多
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* 爆发日期 */}
|
||||
{concept.outbreak_dates && concept.outbreak_dates.length > 0 && (
|
||||
<HStack spacing={0.5} ml={concept.stocks?.length ? 1 : 0}>
|
||||
<Icon as={Zap} boxSize={3} color="orange.400" />
|
||||
<Text color="orange.400" fontSize="10px">
|
||||
{concept.outbreak_dates[0]}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
ConceptCard.displayName = 'ConceptCard';
|
||||
|
||||
export default ConceptCard;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ConceptCard } from './ConceptCard';
|
||||
@@ -0,0 +1 @@
|
||||
export { useHotConcepts } from './useHotConcepts';
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* useHotConcepts - 热门概念数据 Hook
|
||||
* 封装数据获取逻辑,支持缓存和错误处理
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import type { Concept, ConceptsApiResponse } from '../types';
|
||||
import { CONFIG } from '../styles';
|
||||
|
||||
interface UseHotConceptsResult {
|
||||
concepts: Concept[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useHotConcepts = (): UseHotConceptsResult => {
|
||||
const [concepts, setConcepts] = useState<Concept[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchConcepts = useCallback(async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getApiBase()}/api/concepts/daily-top?limit=${CONFIG.fetchLimit}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: ConceptsApiResponse = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setConcepts(data.data || []);
|
||||
} else {
|
||||
throw new Error('API returned unsuccessful response');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorInstance = err instanceof Error ? err : new Error('获取热门概念失败');
|
||||
setError(errorInstance);
|
||||
console.error('获取热门概念失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConcepts();
|
||||
}, [fetchConcepts]);
|
||||
|
||||
return {
|
||||
concepts,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchConcepts,
|
||||
};
|
||||
};
|
||||
141
src/components/GlobalSidebar/HotConceptsPanel/index.tsx
Normal file
141
src/components/GlobalSidebar/HotConceptsPanel/index.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* HotConceptsPanel - 热门概念面板
|
||||
* 用于 GlobalSidebar 的热门概念弹窗内容
|
||||
*
|
||||
* 优化点:
|
||||
* 1. 类型拆分到 types.ts
|
||||
* 2. 样式常量拆分到 styles.ts
|
||||
* 3. 工具函数拆分到 utils.ts
|
||||
* 4. 数据获取逻辑拆分到 hooks/useHotConcepts.ts
|
||||
* 5. 概念卡片拆分为 ConceptCard 原子组件,使用 memo 优化
|
||||
* 6. 使用 useCallback 缓存事件处理函数
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, VStack, HStack, Text, Icon, Spinner, Center } from '@chakra-ui/react';
|
||||
import { Flame, ChevronRight, X } from 'lucide-react';
|
||||
import { getConceptHtmlUrl } from '@/utils/textUtils';
|
||||
|
||||
import type { HotConceptsPanelProps, Concept } from './types';
|
||||
import { useHotConcepts } from './hooks';
|
||||
import { ConceptCard } from './components';
|
||||
import { COLORS, CONFIG, scrollbarStyles } from './styles';
|
||||
|
||||
const HotConceptsPanel: React.FC<HotConceptsPanelProps> = ({
|
||||
onConceptClick,
|
||||
onStockClick,
|
||||
onViewStocks,
|
||||
onClose,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { concepts, loading } = useHotConcepts();
|
||||
|
||||
// 缓存事件处理函数
|
||||
const handleConceptClick = useCallback(
|
||||
(concept: Concept): void => {
|
||||
if (onConceptClick) {
|
||||
onConceptClick(concept);
|
||||
} else {
|
||||
const htmlPath = getConceptHtmlUrl(concept.concept_name);
|
||||
window.open(htmlPath, '_blank');
|
||||
}
|
||||
},
|
||||
[onConceptClick]
|
||||
);
|
||||
|
||||
const handleStockClick = useCallback(
|
||||
(e: React.MouseEvent, stockCode: string): void => {
|
||||
e.stopPropagation();
|
||||
if (onStockClick) {
|
||||
onStockClick(stockCode);
|
||||
} else {
|
||||
navigate(`/company/${stockCode}`);
|
||||
}
|
||||
},
|
||||
[onStockClick, navigate]
|
||||
);
|
||||
|
||||
const handleMoreClick = useCallback(() => {
|
||||
navigate('/concepts');
|
||||
}, [navigate]);
|
||||
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<Center py={8}>
|
||||
<Spinner size="md" color={COLORS.accent} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
// 空状态
|
||||
if (concepts.length === 0) {
|
||||
return (
|
||||
<Center py={8}>
|
||||
<Text fontSize="sm" color={COLORS.textSecondary}>
|
||||
暂无热门概念
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* 标题栏 */}
|
||||
<HStack spacing={2} mb={3} justify="space-between">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Flame} boxSize={4} color={COLORS.accent} />
|
||||
<Text fontSize="sm" fontWeight="bold" color={COLORS.textPrimary}>
|
||||
热门概念
|
||||
</Text>
|
||||
<Text fontSize="xs" color={COLORS.textSecondary}>
|
||||
({concepts.length})
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<HStack
|
||||
spacing={0}
|
||||
cursor="pointer"
|
||||
color={COLORS.accentLight}
|
||||
_hover={{ color: COLORS.accentHover }}
|
||||
onClick={handleMoreClick}
|
||||
>
|
||||
<Text fontSize="xs">更多</Text>
|
||||
<Icon as={ChevronRight} boxSize={4} />
|
||||
</HStack>
|
||||
{onClose && (
|
||||
<Icon
|
||||
as={X}
|
||||
boxSize={4}
|
||||
color={COLORS.textSecondary}
|
||||
cursor="pointer"
|
||||
_hover={{ color: COLORS.textPrimary }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 概念列表 */}
|
||||
<Box maxH={CONFIG.listMaxHeight} overflowY="auto" css={scrollbarStyles}>
|
||||
<VStack spacing={1} align="stretch">
|
||||
{concepts.map((concept, index) => (
|
||||
<ConceptCard
|
||||
key={concept.concept_id || index}
|
||||
concept={concept}
|
||||
index={index}
|
||||
onConceptClick={handleConceptClick}
|
||||
onStockClick={handleStockClick}
|
||||
onViewStocks={onViewStocks}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotConceptsPanel;
|
||||
|
||||
// 导出类型供外部使用
|
||||
export type { Concept, HotConceptsPanelProps } from './types';
|
||||
75
src/components/GlobalSidebar/HotConceptsPanel/styles.ts
Normal file
75
src/components/GlobalSidebar/HotConceptsPanel/styles.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* HotConceptsPanel 样式常量
|
||||
*/
|
||||
import type { SystemStyleObject } from '@chakra-ui/react';
|
||||
|
||||
/** 颜色常量 */
|
||||
export const COLORS = {
|
||||
// 主题色
|
||||
accent: 'rgba(139, 92, 246, 0.9)',
|
||||
accentLight: 'rgba(139, 92, 246, 0.8)',
|
||||
accentHover: 'rgba(139, 92, 246, 1)',
|
||||
tagBg: 'rgba(139, 92, 246, 0.35)',
|
||||
|
||||
// 文字颜色
|
||||
textPrimary: 'rgba(255, 255, 255, 0.9)',
|
||||
textSecondary: 'rgba(255, 255, 255, 0.5)',
|
||||
textMuted: 'rgba(255, 255, 255, 0.4)',
|
||||
|
||||
// 涨跌颜色
|
||||
up: '#EF4444',
|
||||
down: '#22C55E',
|
||||
neutral: 'gray',
|
||||
|
||||
// 背景
|
||||
hoverBg: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||
scrollbarThumb: 'rgba(255, 255, 255, 0.2)',
|
||||
} as const;
|
||||
|
||||
/** 配置常量 */
|
||||
export const CONFIG = {
|
||||
/** API 请求数量限制 */
|
||||
fetchLimit: 8,
|
||||
/** 最大显示标签数 */
|
||||
maxTags: 2,
|
||||
/** 最大显示股票数 */
|
||||
maxStocks: 2,
|
||||
/** 列表最大高度 */
|
||||
listMaxHeight: '350px',
|
||||
/** 前几名使用高亮徽章 */
|
||||
topRankCount: 3,
|
||||
} as const;
|
||||
|
||||
/** 滚动条样式 - 用于 css prop */
|
||||
export const scrollbarStyles = {
|
||||
'&::-webkit-scrollbar': { width: '4px' },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: COLORS.scrollbarThumb,
|
||||
borderRadius: '2px',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** 概念卡片样式 */
|
||||
export const conceptCardStyles: SystemStyleObject = {
|
||||
px: 2,
|
||||
py: 1.5,
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.15s',
|
||||
borderBottom: `1px solid ${COLORS.borderColor}`,
|
||||
_hover: { bg: COLORS.hoverBg },
|
||||
_last: { borderBottom: 'none' },
|
||||
};
|
||||
|
||||
/** 链接样式 */
|
||||
export const linkStyles: SystemStyleObject = {
|
||||
color: COLORS.accentLight,
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
textDecoration: 'underline',
|
||||
color: COLORS.accentHover,
|
||||
},
|
||||
};
|
||||
48
src/components/GlobalSidebar/HotConceptsPanel/types.ts
Normal file
48
src/components/GlobalSidebar/HotConceptsPanel/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* HotConceptsPanel 类型定义
|
||||
*/
|
||||
|
||||
/** 相关股票信息 */
|
||||
export interface ConceptStock {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
}
|
||||
|
||||
/** 概念数据 */
|
||||
export interface Concept {
|
||||
concept_id?: string | number;
|
||||
concept_name: string;
|
||||
change_percent: number;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
stocks?: ConceptStock[];
|
||||
stock_count?: number;
|
||||
outbreak_dates?: string[];
|
||||
}
|
||||
|
||||
/** API 响应 */
|
||||
export interface ConceptsApiResponse {
|
||||
success: boolean;
|
||||
data?: Concept[];
|
||||
}
|
||||
|
||||
/** HotConceptsPanel 组件 Props */
|
||||
export interface HotConceptsPanelProps {
|
||||
/** 概念点击回调 */
|
||||
onConceptClick?: (concept: Concept) => void;
|
||||
/** 股票点击回调 */
|
||||
onStockClick?: (stockCode: string) => void;
|
||||
/** 查看更多股票回调 */
|
||||
onViewStocks?: (concept: Concept) => void;
|
||||
/** 关闭面板回调 */
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/** ConceptCard 组件 Props */
|
||||
export interface ConceptCardProps {
|
||||
concept: Concept;
|
||||
index: number;
|
||||
onConceptClick: (concept: Concept) => void;
|
||||
onStockClick: (e: React.MouseEvent, stockCode: string) => void;
|
||||
onViewStocks?: (concept: Concept) => void;
|
||||
}
|
||||
25
src/components/GlobalSidebar/HotConceptsPanel/utils.ts
Normal file
25
src/components/GlobalSidebar/HotConceptsPanel/utils.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* HotConceptsPanel 工具函数
|
||||
*/
|
||||
import { COLORS } from './styles';
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅显示
|
||||
* 复用自 src/views/Company/components/StockQuoteCard/components/formatters.ts
|
||||
*/
|
||||
export const formatChangePercent = (value: number | null | undefined): string => {
|
||||
if (value == null) return '--';
|
||||
const sign = value >= 0 ? '+' : '';
|
||||
return `${sign}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取涨跌颜色
|
||||
* 简化版本,深色主题专用
|
||||
*/
|
||||
export const getChangeColor = (value: number | null | undefined): string => {
|
||||
if (value == null) return COLORS.neutral;
|
||||
if (value > 0) return COLORS.up;
|
||||
if (value < 0) return COLORS.down;
|
||||
return COLORS.neutral;
|
||||
};
|
||||
@@ -23,8 +23,9 @@ import {
|
||||
Text,
|
||||
HStack,
|
||||
Portal,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronLeft, ChevronRight, BarChart2, Star, TrendingUp } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, BarChart2, Star, TrendingUp, Flame } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useGlobalSidebar } from '@/contexts/GlobalSidebarContext';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
@@ -33,11 +34,100 @@ import { Z_INDEX, LAYOUT_SIZE } from '@/layouts/config/layoutConfig';
|
||||
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';
|
||||
import HotConceptsPanel from './HotConceptsPanel';
|
||||
|
||||
/** 股票信息 */
|
||||
interface Stock {
|
||||
stock_code: string;
|
||||
stock_name?: string;
|
||||
}
|
||||
|
||||
/** 事件信息 */
|
||||
interface Event {
|
||||
id: string | number;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/** 评论信息 */
|
||||
interface Comment {
|
||||
id: string | number;
|
||||
event_id: string | number;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
/** 实时行情 */
|
||||
interface RealtimeQuote {
|
||||
price?: number;
|
||||
change_percent?: number;
|
||||
}
|
||||
|
||||
/** 收起菜单 Props */
|
||||
interface CollapsedMenuProps {
|
||||
watchlist: Stock[];
|
||||
realtimeQuotes: Record<string, RealtimeQuote>;
|
||||
followingEvents: Event[];
|
||||
eventComments: Comment[];
|
||||
onToggle: () => void;
|
||||
onStockClick: (stock: Stock) => void;
|
||||
onEventClick: (event: Event) => void;
|
||||
onCommentClick: (comment: Comment) => void;
|
||||
onAddStock: () => void;
|
||||
onAddEvent: () => void;
|
||||
onUnwatch: (stockCode: string) => void;
|
||||
onUnfollow: (eventId: string | number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 热门概念弹窗组件(使用 controlled 模式)
|
||||
*/
|
||||
const HotConceptsPopover: React.FC = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="left-start"
|
||||
isLazy
|
||||
isOpen={isOpen}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<VStack
|
||||
spacing={1}
|
||||
align="center"
|
||||
cursor="pointer"
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||
position="relative"
|
||||
>
|
||||
<Icon as={Flame} boxSize={5} color="rgba(139, 92, 246, 0.9)" />
|
||||
<Text fontSize="xs" color="rgba(255, 255, 255, 0.85)" fontWeight="medium" whiteSpace="nowrap">
|
||||
热门概念
|
||||
</Text>
|
||||
</VStack>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent
|
||||
w="340px"
|
||||
bg="rgba(26, 32, 44, 0.95)"
|
||||
borderColor="rgba(139, 92, 246, 0.3)"
|
||||
boxShadow="0 8px 32px rgba(139, 92, 246, 0.2)"
|
||||
_focus={{ outline: 'none' }}
|
||||
>
|
||||
<PopoverBody p={3}>
|
||||
<HotConceptsPanel onClose={onClose} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 收起状态下的图标菜单(带悬浮弹窗)
|
||||
*/
|
||||
const CollapsedMenu = ({
|
||||
const CollapsedMenu: React.FC<CollapsedMenuProps> = ({
|
||||
watchlist,
|
||||
realtimeQuotes,
|
||||
followingEvents,
|
||||
@@ -234,11 +324,14 @@ const CollapsedMenu = ({
|
||||
>
|
||||
<PopoverCloseButton color="rgba(255, 255, 255, 0.5)" />
|
||||
<PopoverBody p={2}>
|
||||
<HotSectorsRanking title="热门板块" />
|
||||
<HotSectorsRanking title="热门板块" onSectorClick={() => {}} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
|
||||
{/* 热门概念 - 悬浮弹窗 */}
|
||||
<HotConceptsPopover />
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
@@ -246,7 +339,7 @@ const CollapsedMenu = ({
|
||||
/**
|
||||
* GlobalSidebar 主组件
|
||||
*/
|
||||
const GlobalSidebar = () => {
|
||||
const GlobalSidebar: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -319,9 +412,9 @@ const GlobalSidebar = () => {
|
||||
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))}
|
||||
onStockClick={(stock: Stock) => navigate(`/company/${stock.stock_code}`)}
|
||||
onEventClick={(event: Event) => navigate(getEventDetailUrl(event.id))}
|
||||
onCommentClick={(comment: Comment) => navigate(getEventDetailUrl(comment.event_id))}
|
||||
onAddStock={() => navigate('/stocks')}
|
||||
onAddEvent={() => navigate('/community')}
|
||||
onUnwatch={unwatchStock}
|
||||
@@ -337,9 +430,9 @@ const GlobalSidebar = () => {
|
||||
followingEvents={followingEvents}
|
||||
eventComments={eventComments}
|
||||
onToggle={toggle}
|
||||
onStockClick={(stock) => navigate(`/company/${stock.stock_code}`)}
|
||||
onEventClick={(event) => navigate(getEventDetailUrl(event.id))}
|
||||
onCommentClick={(comment) => navigate(getEventDetailUrl(comment.event_id))}
|
||||
onStockClick={(stock: Stock) => navigate(`/company/${stock.stock_code}`)}
|
||||
onEventClick={(event: Event) => navigate(getEventDetailUrl(event.id))}
|
||||
onCommentClick={(comment: Comment) => navigate(getEventDetailUrl(comment.event_id))}
|
||||
onAddStock={() => navigate('/stocks')}
|
||||
onAddEvent={() => navigate('/community')}
|
||||
onUnwatch={unwatchStock}
|
||||
Reference in New Issue
Block a user