diff --git a/src/views/Company/components/ConceptSector/index.js b/src/views/Company/components/ConceptSector/index.js index bc395109..d81229e2 100644 --- a/src/views/Company/components/ConceptSector/index.js +++ b/src/views/Company/components/ConceptSector/index.js @@ -1,7 +1,7 @@ // src/views/Company/components/ConceptSector/index.js -// 个股详情页 - 概念板块 Tab 组件 +// 个股详情页 - 概念板块 Tab 组件(黑金主题) -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, memo } from 'react'; import { Box, VStack, @@ -13,8 +13,6 @@ import { CardBody, Skeleton, SkeletonText, - Alert, - AlertIcon, Divider, Button, Tooltip, @@ -22,10 +20,11 @@ import { TagLabel, Wrap, WrapItem, - useColorModeValue, Icon, Flex, Spacer, + Spinner, + Center, } from '@chakra-ui/react'; import { Layers, @@ -35,12 +34,42 @@ import { Calendar, Hash, Sparkles, + AlertCircle, + RefreshCw, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; import { getConceptHtmlUrl } from '@utils/textUtils'; +// ============================================ +// 黑金主题配置(与其他 Tab 保持一致) +// ============================================ +const THEME = { + bg: '#0A0E17', + cardBg: '#1A1F2E', + cardHoverBg: '#212633', + cardBorder: 'rgba(212, 175, 55, 0.2)', + cardHoverBorder: '#F4D03F', + textPrimary: '#E8E9ED', + textSecondary: '#A0A4B8', + textMuted: '#6B7280', + gold: '#F4D03F', + goldLight: '#FFD54F', + // 涨跌色 + positive: '#EF4444', + negativE: '#22C55E', + positiveBg: 'rgba(239, 68, 68, 0.15)', + negativeBg: 'rgba(34, 197, 94, 0.15)', + // 标签色 + tagBg: 'rgba(212, 175, 55, 0.15)', + tagColor: '#F4D03F', + // 按钮 + buttonBg: '#D4AF37', + buttonText: '#0A0E17', + buttonHoverBg: '#FFD54F', +}; + // API 配置 const API_BASE_URL = process.env.NODE_ENV === 'production' @@ -48,182 +77,234 @@ const API_BASE_URL = : 'http://111.198.58.126:16801'; /** - * 单个概念卡片组件 + * 单个概念卡片组件(黑金主题) */ -const ConceptCard = ({ concept, onClick }) => { - const cardBg = useColorModeValue('white', 'rgba(26, 32, 44, 0.8)'); - const borderColor = useColorModeValue('gray.200', 'rgba(255, 195, 0, 0.2)'); - const textSecondary = useColorModeValue('gray.600', 'gray.400'); - - // 涨跌幅颜色 +const ConceptCard = memo(({ concept, onClick }) => { + // 涨跌幅 const changePct = concept.price_info?.avg_change_pct; const hasChange = changePct !== null && changePct !== undefined; const isPositive = changePct > 0; - const changeColor = isPositive ? 'red.500' : changePct < 0 ? 'green.500' : 'gray.500'; - const changeBgColor = isPositive - ? 'rgba(239, 68, 68, 0.1)' - : changePct < 0 - ? 'rgba(34, 197, 94, 0.1)' - : 'rgba(128, 128, 128, 0.1)'; + const isNegative = changePct < 0; - // 层级信息 + // 层级路径 const hierarchy = concept.hierarchy; const hierarchyPath = hierarchy ? [hierarchy.lv1, hierarchy.lv2, hierarchy.lv3].filter(Boolean).join(' > ') : null; return ( - - - - {/* 头部:概念名称 + 涨跌幅 */} - - - - - - {concept.concept} - - - {hierarchyPath && ( - - {hierarchyPath} - - )} - - - {hasChange && ( - + {/* 头部:概念名称 + 涨跌幅 */} + + + + + - - - - {isPositive ? '+' : ''} - {changePct.toFixed(2)}% - - - - )} - - - {/* 描述 */} - {concept.description && ( - - {concept.description} - - )} - - {/* 标签 */} - {concept.tags && concept.tags.length > 0 && ( - - {concept.tags.slice(0, 4).map((tag, idx) => ( - - - {tag} - - - ))} - {concept.tags.length > 4 && ( - - - +{concept.tags.length - 4} - - - )} - - )} - - - - {/* 底部信息 */} - - - - {concept.stock_count || 0} 只成分股 + {concept.concept} + - {concept.price_info?.trade_date && ( - - - {concept.price_info.trade_date} - + {hierarchyPath && ( + + {hierarchyPath} + )} + + + {hasChange && ( + + + + + {isPositive ? '+' : ''} + {changePct.toFixed(2)}% + + + + )} + + + {/* 描述 */} + {concept.description && ( + + {concept.description} + + )} + + {/* 标签 */} + {concept.tags && concept.tags.length > 0 && ( + + {concept.tags.slice(0, 4).map((tag, idx) => ( + + + {tag} + + + ))} + {concept.tags.length > 4 && ( + + + +{concept.tags.length - 4} + + + )} + + )} + + + + {/* 底部信息 */} + + + + {concept.stock_count || 0} 只成分股 - - - + {concept.price_info?.trade_date && ( + + + {concept.price_info.trade_date} + + )} + + + ); -}; +}); + +ConceptCard.displayName = 'ConceptCard'; /** - * 骨架屏加载组件 + * 骨架屏加载组件(黑金主题) */ const ConceptSkeleton = () => ( {[...Array(6)].map((_, i) => ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ))} ); /** - * 概念板块组件 + * 空状态组件 + */ +const EmptyState = () => ( +
+ + + + 暂无关联概念数据 + + +
+); + +/** + * 错误状态组件 + */ +const ErrorState = ({ onRetry }) => ( +
+ + + + 获取概念数据失败 + + + +
+); + +/** + * 概念板块组件(黑金主题) * @param {Object} props * @param {string} props.stockCode - 股票代码 */ -const ConceptSector = ({ stockCode }) => { +const ConceptSector = memo(({ stockCode }) => { const navigate = useNavigate(); const [concepts, setConcepts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const bgColor = useColorModeValue('gray.50', 'transparent'); - const headerBg = useColorModeValue('white', 'rgba(26, 32, 44, 0.6)'); - const textColor = useColorModeValue('gray.800', 'white'); - const textSecondary = useColorModeValue('gray.600', 'gray.400'); - /** * 获取股票关联的概念 */ @@ -244,7 +325,7 @@ const ConceptSector = ({ stockCode }) => { setConcepts(data.concepts || []); } catch (err) { logger.error('ConceptSector', 'fetchStockConcepts', err, { stockCode }); - setError('获取概念数据失败,请稍后重试'); + setError('获取概念数据失败'); } finally { setLoading(false); } @@ -257,14 +338,10 @@ const ConceptSector = ({ stockCode }) => { /** * 点击概念跳转 */ - const handleConceptClick = useCallback( - (concept) => { - // 跳转到概念详情页(外部链接) - const url = getConceptHtmlUrl(concept.concept); - window.open(url, '_blank'); - }, - [] - ); + const handleConceptClick = useCallback((concept) => { + const url = getConceptHtmlUrl(concept.concept); + window.open(url, '_blank'); + }, []); /** * 跳转到概念中心 @@ -273,7 +350,7 @@ const ConceptSector = ({ stockCode }) => { navigate('/concept'); }, [navigate]); - // 按涨跌幅排序概念 + // 按涨跌幅排序 const sortedConcepts = [...concepts].sort((a, b) => { const aChange = a.price_info?.avg_change_pct ?? -Infinity; const bChange = b.price_info?.avg_change_pct ?? -Infinity; @@ -282,48 +359,53 @@ const ConceptSector = ({ stockCode }) => { // 统计信息 const totalConcepts = concepts.length; - const positiveConcepts = concepts.filter( - (c) => c.price_info?.avg_change_pct > 0 - ).length; - const negativeConcepts = concepts.filter( - (c) => c.price_info?.avg_change_pct < 0 - ).length; + const positiveConcepts = concepts.filter((c) => c.price_info?.avg_change_pct > 0).length; + const negativeConcepts = concepts.filter((c) => c.price_info?.avg_change_pct < 0).length; return ( - - {/* 头部统计信息 */} - - - + + + + {/* 头部统计信息 */} + - - + + 概念板块 - + - + {totalConcepts} - + 个概念 - + + + - - + + {positiveConcepts} + - - + + {negativeConcepts} @@ -331,11 +413,12 @@ const ConceptSector = ({ stockCode }) => { - + - - - {/* 内容区域 */} - {loading ? ( - - ) : error ? ( - - - {error} - - - ) : concepts.length === 0 ? ( - - - 暂无关联概念数据 - - ) : ( - - {sortedConcepts.map((concept, index) => ( - handleConceptClick(concept)} - /> - ))} - - )} - + {/* 内容区域 */} + {loading ? ( + + ) : error ? ( + + ) : concepts.length === 0 ? ( + + ) : ( + + {sortedConcepts.map((concept, index) => ( + handleConceptClick(concept)} + /> + ))} + + )} + + + ); -}; +}); + +ConceptSector.displayName = 'ConceptSector'; export default ConceptSector;