Files
vf_react/src/views/Community/components/DynamicNewsDetail/StockListItem.js
2025-11-07 19:29:19 +08:00

334 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/components/DynamicNewsDetail/StockListItem.js
// 股票卡片组件(融合表格功能的卡片样式)
import React, { useState } from 'react';
import {
Box,
Flex,
VStack,
HStack,
Text,
Button,
IconButton,
Collapse,
Tooltip,
Badge,
useColorModeValue,
} from '@chakra-ui/react';
import { StarIcon } from '@chakra-ui/icons';
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
import MiniKLineChart from './MiniKLineChart';
import StockChartModal from '../../../../components/StockChart/StockChartModal';
import CitedContent from '../../../../components/Citation/CitedContent';
import { getChangeColor } from '../../../../utils/colorUtils';
/**
* 股票卡片组件
* @param {Object} props
* @param {Object} props.stock - 股票对象
* @param {string} props.stock.stock_name - 股票名称
* @param {string} props.stock.stock_code - 股票代码
* @param {string} props.stock.relation_desc - 关联描述
* @param {Object} props.quote - 股票行情数据(可选)
* @param {number} props.quote.change - 涨跌幅
* @param {string} props.eventTime - 事件时间(可选)
* @param {boolean} props.isInWatchlist - 是否在自选股中
* @param {Function} props.onWatchlistToggle - 切换自选股回调
*/
const StockListItem = ({
stock,
quote = null,
eventTime = null,
isInWatchlist = false,
onWatchlistToggle
}) => {
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');
const codeColor = useColorModeValue('blue.600', 'blue.300');
const nameColor = useColorModeValue('gray.700', 'gray.300');
const descColor = useColorModeValue('gray.600', 'gray.400');
const dividerColor = useColorModeValue('gray.200', 'gray.600');
const [isDescExpanded, setIsDescExpanded] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleViewDetail = () => {
const stockCode = stock.stock_code.split('.')[0];
window.open(`https://valuefrontier.cn/company?scode=${stockCode}`, '_blank');
};
const handleWatchlistClick = (e) => {
e.stopPropagation();
onWatchlistToggle?.(stock.stock_code, isInWatchlist);
};
// 格式化涨跌幅显示
const formatChange = (value) => {
if (value === null || value === undefined || isNaN(value)) return '--';
const prefix = value > 0 ? '+' : '';
return `${prefix}${parseFloat(value).toFixed(2)}%`;
};
// 使用工具函数获取涨跌幅颜色(已从 colorUtils 导入)
// 获取涨跌幅数据(优先使用 quotefallback 到 stock
const change = quote?.change ?? stock.daily_change ?? null;
// 处理关联描述
const getRelationDesc = () => {
const relationDesc = stock.relation_desc;
if (!relationDesc) return '--';
if (typeof relationDesc === 'string') {
return relationDesc;
} else if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
// 新格式:{data: [{query_part: "...", sentences: "..."}]}
return relationDesc.data
.map(item => item.query_part || item.sentences || '')
.filter(s => s)
.join('') || '--';
}
return '--';
};
const relationText = getRelationDesc();
const maxLength = 50; // 收缩时显示的最大字符数
const needTruncate = relationText && relationText !== '--' && relationText.length > maxLength;
return (
<>
<Box
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
p={3}
position="relative"
overflow="visible"
_before={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
bgGradient: 'linear(to-r, blue.400, purple.500, pink.500)',
borderTopLeftRadius: 'lg',
borderTopRightRadius: 'lg',
}}
_hover={{
boxShadow: 'lg',
borderColor: 'blue.300',
}}
transition="all 0.2s"
>
{/* 单行紧凑布局:名称+涨跌幅 | 分时图 | K线图 | 关联描述 */}
<HStack spacing={3} align="stretch">
{/* 左侧:股票名称 + 涨跌幅(垂直排列) - 收窄 */}
<VStack
align="stretch"
spacing={1}
minW="100px"
maxW="120px"
justify="center"
flexShrink={0}
>
<Tooltip
label="点击查看股票详情"
placement="top"
hasArrow
bg="blue.600"
color="white"
fontSize="xs"
>
<Text
fontSize="sm"
fontWeight="bold"
color={codeColor}
noOfLines={1}
cursor="pointer"
onClick={handleViewDetail}
_hover={{ textDecoration: 'underline' }}
>
{stock.stock_name}
</Text>
</Tooltip>
<HStack spacing={1} align="center">
<Text
fontSize="lg"
fontWeight="bold"
color={getChangeColor(change)}
>
{formatChange(change)}
</Text>
{onWatchlistToggle && (
<IconButton
size="xs"
variant={isInWatchlist ? 'solid' : 'ghost'}
colorScheme={isInWatchlist ? 'yellow' : 'gray'}
icon={<StarIcon />}
onClick={handleWatchlistClick}
aria-label={isInWatchlist ? '已关注' : '加自选'}
borderRadius="full"
/>
)}
</HStack>
</VStack>
{/* 分时图 - 固定宽度 */}
<Box
w="160px"
borderWidth="1px"
borderColor={useColorModeValue('blue.100', 'blue.700')}
borderRadius="md"
p={2}
bg={useColorModeValue('blue.50', 'blue.900')}
onClick={(e) => {
e.stopPropagation();
setIsModalOpen(true);
}}
cursor="pointer"
flexShrink={0}
_hover={{
borderColor: useColorModeValue('blue.300', 'blue.500'),
boxShadow: 'sm'
}}
transition="all 0.2s"
>
<Text
fontSize="xs"
color={useColorModeValue('blue.700', 'blue.200')}
mb={1}
fontWeight="semibold"
>
📈 分时
</Text>
<MiniTimelineChart
stockCode={stock.stock_code}
eventTime={eventTime}
/>
</Box>
{/* K线图 - 固定宽度 */}
<Box
w="160px"
borderWidth="1px"
borderColor={useColorModeValue('purple.100', 'purple.700')}
borderRadius="md"
p={2}
bg={useColorModeValue('purple.50', 'purple.900')}
onClick={(e) => {
e.stopPropagation();
setIsModalOpen(true);
}}
cursor="pointer"
flexShrink={0}
_hover={{
borderColor: useColorModeValue('purple.300', 'purple.500'),
boxShadow: 'sm'
}}
transition="all 0.2s"
>
<Text
fontSize="xs"
color={useColorModeValue('purple.700', 'purple.200')}
mb={1}
fontWeight="semibold"
>
📊 日线
</Text>
<MiniKLineChart
stockCode={stock.stock_code}
eventTime={eventTime}
/>
</Box>
{/* 关联描述 - 升级和降级处理 */}
{stock.relation_desc && (
<Box flex={1} minW={0}>
{stock.relation_desc?.data ? (
// 升级:带引用来源的版本
<CitedContent
data={stock.relation_desc}
title=""
showAIBadge={true}
containerStyle={{
backgroundColor: useColorModeValue('#f7fafc', 'rgba(45, 55, 72, 0.6)'),
borderRadius: '8px',
padding: '0',
}}
/>
) : (
// 降级:纯文本版本(保留展开/收起功能)
<Tooltip
label={isDescExpanded ? "点击收起" : "点击展开完整描述"}
placement="top"
hasArrow
bg="gray.600"
color="white"
fontSize="xs"
>
<Box
onClick={(e) => {
e.stopPropagation();
setIsDescExpanded(!isDescExpanded);
}}
cursor="pointer"
px={3}
py={2}
bg={useColorModeValue('gray.50', 'gray.700')}
borderRadius="md"
_hover={{
bg: useColorModeValue('gray.100', 'gray.600'),
}}
transition="background 0.2s"
position="relative"
>
{/* 去掉"关联描述"标题 */}
<Collapse in={isDescExpanded} startingHeight={40}>
<Text
fontSize="sm"
color={nameColor}
lineHeight="1.6"
>
{relationText}
</Text>
</Collapse>
{/* 提示信息 */}
{isDescExpanded && (
<Text
fontSize="xs"
color="gray.500"
mt={2}
fontStyle="italic"
>
AI生成仅供参考
</Text>
)}
</Box>
</Tooltip>
)}
</Box>
)}
</HStack>
</Box>
{/* 股票详情弹窗 - 未打开时不渲染 */}
{isModalOpen && (
<StockChartModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
stock={stock}
eventTime={eventTime}
size="6xl"
/>
)}
</>
);
};
export default StockListItem;