community增加事件详情
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// src/views/Company/components/ConceptSector/index.js
|
// 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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
CardBody,
|
CardBody,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
Alert,
|
|
||||||
AlertIcon,
|
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -22,10 +20,11 @@ import {
|
|||||||
TagLabel,
|
TagLabel,
|
||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
useColorModeValue,
|
|
||||||
Icon,
|
Icon,
|
||||||
Flex,
|
Flex,
|
||||||
Spacer,
|
Spacer,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
Layers,
|
Layers,
|
||||||
@@ -35,12 +34,42 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Hash,
|
Hash,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
import { getConceptHtmlUrl } from '@utils/textUtils';
|
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 配置
|
// API 配置
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
process.env.NODE_ENV === 'production'
|
process.env.NODE_ENV === 'production'
|
||||||
@@ -48,182 +77,234 @@ const API_BASE_URL =
|
|||||||
: 'http://111.198.58.126:16801';
|
: 'http://111.198.58.126:16801';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个概念卡片组件
|
* 单个概念卡片组件(黑金主题)
|
||||||
*/
|
*/
|
||||||
const ConceptCard = ({ concept, onClick }) => {
|
const ConceptCard = memo(({ 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 changePct = concept.price_info?.avg_change_pct;
|
const changePct = concept.price_info?.avg_change_pct;
|
||||||
const hasChange = changePct !== null && changePct !== undefined;
|
const hasChange = changePct !== null && changePct !== undefined;
|
||||||
const isPositive = changePct > 0;
|
const isPositive = changePct > 0;
|
||||||
const changeColor = isPositive ? 'red.500' : changePct < 0 ? 'green.500' : 'gray.500';
|
const isNegative = changePct < 0;
|
||||||
const changeBgColor = isPositive
|
|
||||||
? 'rgba(239, 68, 68, 0.1)'
|
|
||||||
: changePct < 0
|
|
||||||
? 'rgba(34, 197, 94, 0.1)'
|
|
||||||
: 'rgba(128, 128, 128, 0.1)';
|
|
||||||
|
|
||||||
// 层级信息
|
// 层级路径
|
||||||
const hierarchy = concept.hierarchy;
|
const hierarchy = concept.hierarchy;
|
||||||
const hierarchyPath = hierarchy
|
const hierarchyPath = hierarchy
|
||||||
? [hierarchy.lv1, hierarchy.lv2, hierarchy.lv3].filter(Boolean).join(' > ')
|
? [hierarchy.lv1, hierarchy.lv2, hierarchy.lv3].filter(Boolean).join(' > ')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Box
|
||||||
bg={cardBg}
|
bg={THEME.cardBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={borderColor}
|
borderColor={THEME.cardBorder}
|
||||||
borderRadius="xl"
|
borderRadius="lg"
|
||||||
overflow="hidden"
|
p={4}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
transition="all 0.3s"
|
transition="all 0.2s ease"
|
||||||
_hover={{
|
_hover={{
|
||||||
transform: 'translateY(-4px)',
|
bg: THEME.cardHoverBg,
|
||||||
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.15)',
|
borderColor: THEME.cardHoverBorder,
|
||||||
borderColor: 'blue.400',
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: `0 4px 20px rgba(212, 175, 55, 0.15)`,
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<CardBody p={4}>
|
<VStack align="stretch" spacing={3}>
|
||||||
<VStack align="stretch" spacing={3}>
|
{/* 头部:概念名称 + 涨跌幅 */}
|
||||||
{/* 头部:概念名称 + 涨跌幅 */}
|
<HStack justify="space-between" align="flex-start">
|
||||||
<HStack justify="space-between" align="flex-start">
|
<VStack align="start" spacing={1} flex={1} minW={0}>
|
||||||
<VStack align="start" spacing={1} flex={1}>
|
<HStack spacing={2}>
|
||||||
<HStack spacing={2}>
|
<Icon as={Layers} boxSize={4} color={THEME.gold} />
|
||||||
<Icon as={Layers} boxSize={4} color="blue.400" />
|
<Text
|
||||||
<Text fontSize="md" fontWeight="bold" color="blue.400" noOfLines={1}>
|
fontSize="md"
|
||||||
{concept.concept}
|
fontWeight="bold"
|
||||||
</Text>
|
color={THEME.gold}
|
||||||
</HStack>
|
noOfLines={1}
|
||||||
{hierarchyPath && (
|
title={concept.concept}
|
||||||
<Text fontSize="xs" color={textSecondary} noOfLines={1}>
|
|
||||||
{hierarchyPath}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{hasChange && (
|
|
||||||
<Box
|
|
||||||
bg={changeBgColor}
|
|
||||||
px={3}
|
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
minW="70px"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
>
|
||||||
<HStack spacing={1} justify="center">
|
{concept.concept}
|
||||||
<Icon
|
</Text>
|
||||||
as={isPositive ? TrendingUp : TrendingDown}
|
|
||||||
boxSize={3}
|
|
||||||
color={changeColor}
|
|
||||||
/>
|
|
||||||
<Text fontSize="sm" fontWeight="bold" color={changeColor}>
|
|
||||||
{isPositive ? '+' : ''}
|
|
||||||
{changePct.toFixed(2)}%
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 描述 */}
|
|
||||||
{concept.description && (
|
|
||||||
<Text fontSize="sm" color={textSecondary} noOfLines={2} lineHeight="1.6">
|
|
||||||
{concept.description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 标签 */}
|
|
||||||
{concept.tags && concept.tags.length > 0 && (
|
|
||||||
<Wrap spacing={1}>
|
|
||||||
{concept.tags.slice(0, 4).map((tag, idx) => (
|
|
||||||
<WrapItem key={idx}>
|
|
||||||
<Tag size="sm" variant="subtle" colorScheme="purple" borderRadius="full">
|
|
||||||
<TagLabel fontSize="xs">{tag}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
</WrapItem>
|
|
||||||
))}
|
|
||||||
{concept.tags.length > 4 && (
|
|
||||||
<WrapItem>
|
|
||||||
<Tag size="sm" variant="outline" colorScheme="gray" borderRadius="full">
|
|
||||||
<TagLabel fontSize="xs">+{concept.tags.length - 4}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
</WrapItem>
|
|
||||||
)}
|
|
||||||
</Wrap>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* 底部信息 */}
|
|
||||||
<HStack justify="space-between" fontSize="xs" color={textSecondary}>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Icon as={Hash} boxSize={3} />
|
|
||||||
<Text>{concept.stock_count || 0} 只成分股</Text>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
{concept.price_info?.trade_date && (
|
{hierarchyPath && (
|
||||||
<HStack spacing={1}>
|
<Text fontSize="xs" color={THEME.textMuted} noOfLines={1}>
|
||||||
<Icon as={Calendar} boxSize={3} />
|
{hierarchyPath}
|
||||||
<Text>{concept.price_info.trade_date}</Text>
|
</Text>
|
||||||
</HStack>
|
|
||||||
)}
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{hasChange && (
|
||||||
|
<Box
|
||||||
|
bg={isPositive ? THEME.positiveBg : isNegative ? THEME.negativeBg : 'rgba(107, 114, 128, 0.15)'}
|
||||||
|
px={3}
|
||||||
|
py={1.5}
|
||||||
|
borderRadius="md"
|
||||||
|
minW="75px"
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
<HStack spacing={1} justify="center">
|
||||||
|
<Icon
|
||||||
|
as={isPositive ? TrendingUp : TrendingDown}
|
||||||
|
boxSize={3.5}
|
||||||
|
color={isPositive ? THEME.positive : isNegative ? '#22C55E' : THEME.textMuted}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={isPositive ? THEME.positive : isNegative ? '#22C55E' : THEME.textMuted}
|
||||||
|
>
|
||||||
|
{isPositive ? '+' : ''}
|
||||||
|
{changePct.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
{concept.description && (
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary} noOfLines={2} lineHeight="1.6">
|
||||||
|
{concept.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 标签 */}
|
||||||
|
{concept.tags && concept.tags.length > 0 && (
|
||||||
|
<Wrap spacing={1.5}>
|
||||||
|
{concept.tags.slice(0, 4).map((tag, idx) => (
|
||||||
|
<WrapItem key={idx}>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
bg={THEME.tagBg}
|
||||||
|
color={THEME.tagColor}
|
||||||
|
borderRadius="full"
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
<TagLabel fontSize="xs">{tag}</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
{concept.tags.length > 4 && (
|
||||||
|
<WrapItem>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
bg="rgba(107, 114, 128, 0.15)"
|
||||||
|
color={THEME.textMuted}
|
||||||
|
borderRadius="full"
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
<TagLabel fontSize="xs">+{concept.tags.length - 4}</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
</WrapItem>
|
||||||
|
)}
|
||||||
|
</Wrap>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider borderColor={THEME.cardBorder} />
|
||||||
|
|
||||||
|
{/* 底部信息 */}
|
||||||
|
<HStack justify="space-between" fontSize="xs" color={THEME.textMuted}>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon as={Hash} boxSize={3} />
|
||||||
|
<Text>{concept.stock_count || 0} 只成分股</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
{concept.price_info?.trade_date && (
|
||||||
</CardBody>
|
<HStack spacing={1}>
|
||||||
</Card>
|
<Icon as={Calendar} boxSize={3} />
|
||||||
|
<Text>{concept.price_info.trade_date}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ConceptCard.displayName = 'ConceptCard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 骨架屏加载组件
|
* 骨架屏加载组件(黑金主题)
|
||||||
*/
|
*/
|
||||||
const ConceptSkeleton = () => (
|
const ConceptSkeleton = () => (
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||||
{[...Array(6)].map((_, i) => (
|
{[...Array(6)].map((_, i) => (
|
||||||
<Card key={i} borderRadius="xl" overflow="hidden">
|
<Box
|
||||||
<CardBody p={4}>
|
key={i}
|
||||||
<VStack align="stretch" spacing={3}>
|
bg={THEME.cardBg}
|
||||||
<HStack justify="space-between">
|
borderWidth="1px"
|
||||||
<Skeleton height="24px" width="120px" />
|
borderColor={THEME.cardBorder}
|
||||||
<Skeleton height="28px" width="70px" borderRadius="md" />
|
borderRadius="lg"
|
||||||
</HStack>
|
p={4}
|
||||||
<SkeletonText noOfLines={2} spacing={2} />
|
>
|
||||||
<HStack spacing={2}>
|
<VStack align="stretch" spacing={3}>
|
||||||
<Skeleton height="20px" width="50px" borderRadius="full" />
|
<HStack justify="space-between">
|
||||||
<Skeleton height="20px" width="50px" borderRadius="full" />
|
<Skeleton height="24px" width="120px" startColor="#2D3748" endColor="#4A5568" />
|
||||||
</HStack>
|
<Skeleton height="32px" width="75px" borderRadius="md" startColor="#2D3748" endColor="#4A5568" />
|
||||||
<Divider />
|
</HStack>
|
||||||
<HStack justify="space-between">
|
<SkeletonText noOfLines={2} spacing={2} startColor="#2D3748" endColor="#4A5568" />
|
||||||
<Skeleton height="16px" width="80px" />
|
<HStack spacing={2}>
|
||||||
<Skeleton height="16px" width="80px" />
|
<Skeleton height="22px" width="50px" borderRadius="full" startColor="#2D3748" endColor="#4A5568" />
|
||||||
</HStack>
|
<Skeleton height="22px" width="50px" borderRadius="full" startColor="#2D3748" endColor="#4A5568" />
|
||||||
</VStack>
|
</HStack>
|
||||||
</CardBody>
|
<Divider borderColor={THEME.cardBorder} />
|
||||||
</Card>
|
<HStack justify="space-between">
|
||||||
|
<Skeleton height="16px" width="80px" startColor="#2D3748" endColor="#4A5568" />
|
||||||
|
<Skeleton height="16px" width="80px" startColor="#2D3748" endColor="#4A5568" />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 概念板块组件
|
* 空状态组件
|
||||||
|
*/
|
||||||
|
const EmptyState = () => (
|
||||||
|
<Center py={12}>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Icon as={AlertCircle} boxSize={12} color={THEME.textMuted} />
|
||||||
|
<Text color={THEME.textSecondary} fontSize="md">
|
||||||
|
暂无关联概念数据
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误状态组件
|
||||||
|
*/
|
||||||
|
const ErrorState = ({ onRetry }) => (
|
||||||
|
<Center py={12}>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Icon as={AlertCircle} boxSize={12} color={THEME.positive} />
|
||||||
|
<Text color={THEME.textSecondary} fontSize="md">
|
||||||
|
获取概念数据失败
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
bg={THEME.buttonBg}
|
||||||
|
color={THEME.buttonText}
|
||||||
|
_hover={{ bg: THEME.buttonHoverBg }}
|
||||||
|
leftIcon={<RefreshCw size={14} />}
|
||||||
|
onClick={onRetry}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 概念板块组件(黑金主题)
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {string} props.stockCode - 股票代码
|
* @param {string} props.stockCode - 股票代码
|
||||||
*/
|
*/
|
||||||
const ConceptSector = ({ stockCode }) => {
|
const ConceptSector = memo(({ stockCode }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [concepts, setConcepts] = useState([]);
|
const [concepts, setConcepts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
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 || []);
|
setConcepts(data.concepts || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('ConceptSector', 'fetchStockConcepts', err, { stockCode });
|
logger.error('ConceptSector', 'fetchStockConcepts', err, { stockCode });
|
||||||
setError('获取概念数据失败,请稍后重试');
|
setError('获取概念数据失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -257,14 +338,10 @@ const ConceptSector = ({ stockCode }) => {
|
|||||||
/**
|
/**
|
||||||
* 点击概念跳转
|
* 点击概念跳转
|
||||||
*/
|
*/
|
||||||
const handleConceptClick = useCallback(
|
const handleConceptClick = useCallback((concept) => {
|
||||||
(concept) => {
|
const url = getConceptHtmlUrl(concept.concept);
|
||||||
// 跳转到概念详情页(外部链接)
|
window.open(url, '_blank');
|
||||||
const url = getConceptHtmlUrl(concept.concept);
|
}, []);
|
||||||
window.open(url, '_blank');
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跳转到概念中心
|
* 跳转到概念中心
|
||||||
@@ -273,7 +350,7 @@ const ConceptSector = ({ stockCode }) => {
|
|||||||
navigate('/concept');
|
navigate('/concept');
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// 按涨跌幅排序概念
|
// 按涨跌幅排序
|
||||||
const sortedConcepts = [...concepts].sort((a, b) => {
|
const sortedConcepts = [...concepts].sort((a, b) => {
|
||||||
const aChange = a.price_info?.avg_change_pct ?? -Infinity;
|
const aChange = a.price_info?.avg_change_pct ?? -Infinity;
|
||||||
const bChange = b.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 totalConcepts = concepts.length;
|
||||||
const positiveConcepts = concepts.filter(
|
const positiveConcepts = concepts.filter((c) => c.price_info?.avg_change_pct > 0).length;
|
||||||
(c) => c.price_info?.avg_change_pct > 0
|
const negativeConcepts = concepts.filter((c) => c.price_info?.avg_change_pct < 0).length;
|
||||||
).length;
|
|
||||||
const negativeConcepts = concepts.filter(
|
|
||||||
(c) => c.price_info?.avg_change_pct < 0
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box bg={bgColor} minH="400px" p={{ base: 3, md: 4 }}>
|
<Card bg={THEME.cardBg} shadow="md" borderColor={THEME.cardBorder} borderWidth="1px">
|
||||||
{/* 头部统计信息 */}
|
<CardBody p={4}>
|
||||||
<Card bg={headerBg} borderRadius="xl" mb={4} boxShadow="sm">
|
<VStack spacing={4} align="stretch">
|
||||||
<CardBody py={3} px={4}>
|
{/* 头部统计信息 */}
|
||||||
<Flex align="center" wrap="wrap" gap={4}>
|
<Flex align="center" wrap="wrap" gap={4} pb={3} borderBottom="1px solid" borderColor={THEME.cardBorder}>
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Icon as={Sparkles} boxSize={5} color="yellow.400" />
|
<Icon as={Sparkles} boxSize={5} color={THEME.gold} />
|
||||||
<Text fontSize="lg" fontWeight="bold" color={textColor}>
|
<Text fontSize="lg" fontWeight="bold" color={THEME.textPrimary}>
|
||||||
概念板块
|
概念板块
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack spacing={4} flex={1} justify="center">
|
<HStack spacing={4} flex={1} justify="center">
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Text fontSize="sm" color={textSecondary}>
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
共
|
共
|
||||||
</Text>
|
</Text>
|
||||||
<Badge colorScheme="blue" fontSize="sm" px={2}>
|
<Badge
|
||||||
|
bg={THEME.tagBg}
|
||||||
|
color={THEME.tagColor}
|
||||||
|
fontSize="sm"
|
||||||
|
px={2}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
{totalConcepts}
|
{totalConcepts}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fontSize="sm" color={textSecondary}>
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
个概念
|
个概念
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Divider orientation="vertical" h="20px" />
|
|
||||||
|
<Divider orientation="vertical" h="20px" borderColor={THEME.cardBorder} />
|
||||||
|
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<Icon as={TrendingUp} boxSize={4} color="red.400" />
|
<Icon as={TrendingUp} boxSize={4} color={THEME.positive} />
|
||||||
<Text fontSize="sm" color="red.400" fontWeight="medium">
|
<Text fontSize="sm" color={THEME.positive} fontWeight="medium">
|
||||||
{positiveConcepts}
|
{positiveConcepts}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack spacing={1}>
|
<HStack spacing={1}>
|
||||||
<Icon as={TrendingDown} boxSize={4} color="green.400" />
|
<Icon as={TrendingDown} boxSize={4} color="#22C55E" />
|
||||||
<Text fontSize="sm" color="green.400" fontWeight="medium">
|
<Text fontSize="sm" color="#22C55E" fontWeight="medium">
|
||||||
{negativeConcepts}
|
{negativeConcepts}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -331,11 +413,12 @@ const ConceptSector = ({ stockCode }) => {
|
|||||||
|
|
||||||
<Spacer display={{ base: 'none', md: 'block' }} />
|
<Spacer display={{ base: 'none', md: 'block' }} />
|
||||||
|
|
||||||
<Tooltip label="前往概念中心查看更多">
|
<Tooltip label="前往概念中心查看更多" bg={THEME.cardBg} color={THEME.textPrimary}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
bg={THEME.buttonBg}
|
||||||
colorScheme="blue"
|
color={THEME.buttonText}
|
||||||
|
_hover={{ bg: THEME.buttonHoverBg }}
|
||||||
rightIcon={<ChevronRight size={16} />}
|
rightIcon={<ChevronRight size={16} />}
|
||||||
onClick={handleGoToConceptCenter}
|
onClick={handleGoToConceptCenter}
|
||||||
>
|
>
|
||||||
@@ -343,38 +426,31 @@ const ConceptSector = ({ stockCode }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ConceptSkeleton />
|
<ConceptSkeleton />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Alert status="error" borderRadius="xl">
|
<ErrorState onRetry={fetchStockConcepts} />
|
||||||
<AlertIcon />
|
) : concepts.length === 0 ? (
|
||||||
{error}
|
<EmptyState />
|
||||||
<Button size="sm" ml={4} onClick={fetchStockConcepts}>
|
) : (
|
||||||
重试
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||||
</Button>
|
{sortedConcepts.map((concept, index) => (
|
||||||
</Alert>
|
<ConceptCard
|
||||||
) : concepts.length === 0 ? (
|
key={concept.concept_id || index}
|
||||||
<Alert status="info" borderRadius="xl">
|
concept={concept}
|
||||||
<AlertIcon />
|
onClick={() => handleConceptClick(concept)}
|
||||||
暂无关联概念数据
|
/>
|
||||||
</Alert>
|
))}
|
||||||
) : (
|
</SimpleGrid>
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
)}
|
||||||
{sortedConcepts.map((concept, index) => (
|
</VStack>
|
||||||
<ConceptCard
|
</CardBody>
|
||||||
key={concept.concept_id || index}
|
</Card>
|
||||||
concept={concept}
|
|
||||||
onClick={() => handleConceptClick(concept)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ConceptSector.displayName = 'ConceptSector';
|
||||||
|
|
||||||
export default ConceptSector;
|
export default ConceptSector;
|
||||||
|
|||||||
Reference in New Issue
Block a user