295 lines
17 KiB
JavaScript
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; |