Files
vf_react/src/views/LimitAnalyse/components/SectorDetails.js

295 lines
17 KiB
JavaScript

import React, { useState, useRef, useEffect } from 'react';
import {
Box,
Card,
CardHeader,
CardBody,
HStack,
VStack,
Heading,
Text,
Badge,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
IconButton,
Flex,
Circle,
Tag,
TagLabel,
Wrap,
WrapItem,
Button,
useColorModeValue,
Collapse,
useDisclosure,
} from '@chakra-ui/react';
import { StarIcon, ViewIcon, TimeIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import { getFormattedTextProps } from '../../../utils/textUtils';
const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
// 使用 useRef 来维持展开状态,避免重新渲染时重置
const expandedSectorsRef = useRef([]);
const [expandedSectors, setExpandedSectors] = useState([]);
const [isInitialized, setIsInitialized] = useState(false);
// 新增:管理每个股票涨停原因的展开状态
const [expandedStockReasons, setExpandedStockReasons] = useState({});
const cardBg = useColorModeValue('white', 'gray.800');
// 只在组件首次挂载时初始化
useEffect(() => {
if (!isInitialized) {
setIsInitialized(true);
}
}, []);
// 处理展开/收起
const handleAccordionChange = (newExpandedIndexes) => {
expandedSectorsRef.current = newExpandedIndexes;
setExpandedSectors(newExpandedIndexes);
};
// 全部展开/收起
const toggleAllSectors = () => {
if (expandedSectors.length === sortedSectors.length) {
handleAccordionChange([]);
} else {
handleAccordionChange(sortedSectors.map((_, index) => index));
}
};
// 新增:切换股票涨停原因的展开状态
const toggleStockReason = (stockCode) => {
setExpandedStockReasons(prev => ({
...prev,
[stockCode]: !prev[stockCode]
}));
};
const getSectorColorScheme = (sector) => {
if (sector === '公告') return 'orange';
if (sector === '其他') return 'gray';
if (sector.includes('锂电') || sector.includes('电池')) return 'green';
if (sector.includes('AI') || sector.includes('芯片')) return 'blue';
if (sector.includes('天然气') || sector.includes('石油')) return 'red';
if (sector.includes('医药') || sector.includes('医疗')) return 'purple';
if (sector.includes('新能源') || sector.includes('光伏')) return 'teal';
if (sector.includes('金融') || sector.includes('银行')) return 'yellow';
return 'cyan';
};
const formatStockTime = (stock) => {
if (stock.formatted_time) return stock.formatted_time;
if (stock.zt_time) {
const time = stock.zt_time.split(' ')[1];
return time || stock.zt_time;
}
return '-';
};
const getContinuousDaysBadgeColor = (days) => {
if (!days) return 'gray';
if (days.includes('5') || days.includes('6') || days.includes('7')) return 'red';
if (days.includes('3') || days.includes('4')) return 'orange';
if (days.includes('2')) return 'yellow';
return 'green';
};
return (
<Card bg={cardBg} borderRadius="xl" boxShadow="xl">
<CardHeader bg="blue.500" color="white" borderTopRadius="xl">
<Flex justify="space-between" align="center">
<HStack spacing={3}>
<Heading size="md">板块详情</Heading>
</HStack>
<HStack spacing={2}>
<Badge bg="whiteAlpha.900" color="blue.500" fontSize="md" px={3}>
{sortedSectors.length} 个板块
</Badge>
<Badge bg="red.500" color="white" fontSize="md" px={3}>
{totalStocks} 只涨停
</Badge>
</HStack>
</Flex>
</CardHeader>
<CardBody maxH="700px" overflowY="auto" css={{
'&::-webkit-scrollbar': { width: '8px' },
'&::-webkit-scrollbar-track': { background: '#f1f1f1' },
'&::-webkit-scrollbar-thumb': { background: '#888', borderRadius: '4px' },
'&::-webkit-scrollbar-thumb:hover': { background: '#555' },
}}>
<Accordion
allowMultiple
index={expandedSectors}
onChange={handleAccordionChange}
>
{sortedSectors.map(([sector, data], index) => {
const colorScheme = getSectorColorScheme(sector);
const isSpecial = sector === '公告' || sector === '其他';
const isExpanded = expandedSectors.includes(index);
return (
<AccordionItem key={`${sector}-${index}`} border="none" mb={3}>
<h2>
<AccordionButton
bg={isExpanded ? `${colorScheme}.500` : isSpecial ? `${colorScheme}.50` : 'gray.50'}
color={isExpanded ? 'white' : 'inherit'}
borderRadius="lg"
_hover={{
bg: isExpanded ? `${colorScheme}.600` : isSpecial ? `${colorScheme}.100` : 'gray.100'
}}
position="relative"
h="auto"
py={3}
>
<Box flex="1" textAlign="left">
<HStack spacing={3}>
<Circle
size="35px"
bg={isExpanded ? 'white' : `${colorScheme}.500`}
color={isExpanded ? `${colorScheme}.500` : 'white'}
>
<Text fontWeight="bold" fontSize="md">{index + 1}</Text>
</Circle>
{sector === '公告' && <StarIcon />}
<VStack align="start" spacing={0}>
<Text fontWeight="bold" fontSize="lg">{sector}</Text>
<Text fontSize="sm" opacity={0.9}>
{data.count} 只涨停
{data.stocks && data.stocks[0]?.zt_time && (
<Text as="span" fontSize="xs" ml={2}>
首板: {formatStockTime(data.stocks[0])}
</Text>
)}
</Text>
</VStack>
</HStack>
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4} pt={3}>
<VStack align="stretch" spacing={2}>
{data.stocks
.sort((a, b) => (a.zt_time || '').localeCompare(b.zt_time || ''))
.map((stock, idx) => (
<Box
key={`${stock.scode}-${idx}`}
p={4}
borderRadius="lg"
bg="white"
border="1px solid"
borderColor="gray.200"
borderLeft="4px solid"
borderLeftColor={`${colorScheme}.400`}
_hover={{
transform: 'translateX(5px)',
boxShadow: 'lg',
borderLeftColor: `${colorScheme}.600`,
bg: 'gray.50'
}}
transition="all 0.2s"
cursor="pointer"
onClick={() => onStockClick && onStockClick(stock)}
>
<Flex justify="space-between" align="start">
<VStack align="start" spacing={2} flex={1}>
<HStack spacing={2} wrap="wrap">
<Text fontWeight="bold" fontSize="lg">{stock.sname}</Text>
<Badge colorScheme="purple" fontSize="sm">{stock.scode}</Badge>
{stock.continuous_days && (
<Badge
colorScheme={getContinuousDaysBadgeColor(stock.continuous_days)}
variant="solid"
fontSize="sm"
>
{stock.continuous_days}
</Badge>
)}
</HStack>
<Collapse in={expandedStockReasons[stock.scode]}>
<Box mt={2} p={3} bg="gray.50" borderRadius="md" border="1px solid" borderColor="gray.200">
<Text fontSize="sm" color="gray.700" fontWeight="bold">
涨停原因:
</Text>
<Text
fontSize="sm"
color="gray.600"
noOfLines={3}
{...getFormattedTextProps(stock.brief || stock.summary || '暂无涨停原因').props}
>
{getFormattedTextProps(stock.brief || stock.summary || '暂无涨停原因').children}
</Text>
</Box>
</Collapse>
<HStack spacing={4} fontSize="xs" color="gray.500">
<HStack spacing={1}>
<TimeIcon boxSize={3} />
<Text>涨停: {formatStockTime(stock)}</Text>
</HStack>
{stock.first_time && (
<Text>首板: {stock.first_time.split(' ')[0]}</Text>
)}
{stock.change_pct && (
<Text color="red.500" fontWeight="bold">
涨幅: {stock.change_pct}%
</Text>
)}
</HStack>
{stock.core_sectors && stock.core_sectors.length > 0 && (
<Wrap spacing={1} mt={1}>
{stock.core_sectors.slice(0, 5).map((s, i) => (
<WrapItem key={`${s}-${i}`}>
<Tag
size="sm"
colorScheme={getSectorColorScheme(s)}
variant="subtle"
>
<TagLabel fontSize="xs">{s}</TagLabel>
</Tag>
</WrapItem>
))}
{stock.core_sectors.length > 5 && (
<WrapItem>
<Tag size="sm" colorScheme="gray">
<TagLabel fontSize="xs">
+{stock.core_sectors.length - 5}
</TagLabel>
</Tag>
</WrapItem>
)}
</Wrap>
)}
</VStack>
<IconButton
icon={expandedStockReasons[stock.scode] ? <ChevronUpIcon /> : <ChevronDownIcon />}
size="sm"
variant="ghost"
colorScheme={colorScheme}
aria-label={expandedStockReasons[stock.scode] ? "收起原因" : "展开原因"}
onClick={() => toggleStockReason(stock.scode)}
/>
</Flex>
</Box>
))}
</VStack>
</AccordionPanel>
</AccordionItem>
);
})}
</Accordion>
</CardBody>
</Card>
);
};
export default SectorDetails;