Files
vf_react/src/views/Community/components/DynamicNewsDetail/StockListItem.js
2025-11-27 16:40:35 +08:00

415 lines
13 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 { Tag } from 'antd';
import { RobotOutlined } from '@ant-design/icons';
import MiniTimelineChart from '../StockDetailPanel/components/MiniTimelineChart';
import MiniKLineChart from './MiniKLineChart';
import TimelineChartModal from '@components/StockChart/TimelineChartModal';
import KLineChartModal from '@components/StockChart/KLineChartModal';
import { getChangeColor } from '@utils/colorUtils';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
/**
* 股票卡片组件
* @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 = PROFESSIONAL_COLORS.background.card;
const borderColor = PROFESSIONAL_COLORS.border.default;
const codeColor = '#3B82F6';
const nameColor = PROFESSIONAL_COLORS.text.primary;
const descColor = PROFESSIONAL_COLORS.text.secondary;
const dividerColor = PROFESSIONAL_COLORS.border.default;
const [isDescExpanded, setIsDescExpanded] = useState(false);
const [isTimelineModalOpen, setIsTimelineModalOpen] = useState(false);
const [isKLineModalOpen, setIsKLineModalOpen] = 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={2} align="center" flexWrap="wrap">
{/* 左侧:股票信息区 */}
<HStack spacing={2} overflow="hidden">
{/* 股票代码 + 名称 + 涨跌幅 */}
<VStack
align="stretch"
spacing={1}
minW="95px"
maxW="110px"
justify="center"
flexShrink={0}
>
<Tooltip
label="点击查看股票详情"
placement="top"
hasArrow
bg="blue.600"
color="white"
fontSize="xs"
>
<VStack spacing={0} align="stretch">
<Text
fontSize="xs"
color={codeColor}
noOfLines={1}
cursor="pointer"
onClick={handleViewDetail}
_hover={{ textDecoration: 'underline' }}
>
{stock.stock_code}
</Text>
<Text
fontSize="xs"
fontWeight="bold"
color={nameColor}
noOfLines={1}
cursor="pointer"
onClick={handleViewDetail}
_hover={{ textDecoration: 'underline' }}
>
{stock.stock_name}
</Text>
</VStack>
</Tooltip>
<HStack spacing={1} align="center">
<Text
fontSize="md"
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>
{/* 分时图 - 自适应 */}
<VStack
flex={1}
minW="80px"
maxW="150px"
borderWidth="1px"
borderColor="rgba(59, 130, 246, 0.3)"
borderRadius="md"
px={2}
py={1.5}
bg="rgba(59, 130, 246, 0.1)"
onClick={(e) => {
e.stopPropagation();
setIsTimelineModalOpen(true);
}}
cursor="pointer"
align="stretch"
spacing={0}
_hover={{
borderColor: '#3B82F6',
boxShadow: '0 0 10px rgba(59, 130, 246, 0.3)',
transform: 'translateY(-1px)'
}}
transition="all 0.2s"
>
<Text
fontSize="10px"
color="#3B82F6"
fontWeight="semibold"
whiteSpace="nowrap"
mb={0.5}
>
📈 分时
</Text>
<Box h="28px">
<MiniTimelineChart
stockCode={stock.stock_code}
eventTime={eventTime}
/>
</Box>
</VStack>
{/* K线图 - 自适应 */}
<VStack
flex={1}
minW="80px"
maxW="150px"
borderWidth="1px"
borderColor="rgba(168, 85, 247, 0.3)"
borderRadius="md"
px={2}
py={1.5}
bg="rgba(168, 85, 247, 0.1)"
onClick={(e) => {
e.stopPropagation();
setIsKLineModalOpen(true);
}}
cursor="pointer"
align="stretch"
spacing={0}
_hover={{
borderColor: '#A855F7',
boxShadow: '0 0 10px rgba(168, 85, 247, 0.3)',
transform: 'translateY(-1px)'
}}
transition="all 0.2s"
>
<Text
fontSize="10px"
color="#A855F7"
fontWeight="semibold"
whiteSpace="nowrap"
mb={0.5}
>
📊 日线
</Text>
<Box h="28px">
<MiniKLineChart
stockCode={stock.stock_code}
eventTime={eventTime}
/>
</Box>
</VStack>
</HStack>
{/* 关联描述 - 升级和降级处理 */}
{stock.relation_desc && (
<Box flex={1} minW={0}>
{stock.relation_desc?.data ? (
// 升级:带引用来源的版本 - 添加折叠功能
<Tooltip
label={isDescExpanded ? "点击收起" : "点击展开完整描述"}
placement="top"
hasArrow
bg="rgba(20, 20, 20, 0.95)"
color={PROFESSIONAL_COLORS.gold[500]}
fontSize="xs"
>
<Box
onClick={(e) => {
e.stopPropagation();
setIsDescExpanded(!isDescExpanded);
}}
cursor="pointer"
bg={PROFESSIONAL_COLORS.background.secondary}
borderRadius="md"
_hover={{
bg: PROFESSIONAL_COLORS.background.cardHover,
}}
transition="background 0.2s"
position="relative"
>
<Collapse in={isDescExpanded} startingHeight={56}>
{/* AI 标识 - 行内显示在文字前面 */}
<Tag
icon={<RobotOutlined />}
color="purple"
style={{
fontSize: 12,
padding: '2px 8px',
marginRight: 8,
verticalAlign: 'middle',
display: 'inline-flex',
}}
>
AI合成
</Tag>
{/* 直接渲染文字内容 */}
<Text
as="span"
fontSize="sm"
color={PROFESSIONAL_COLORS.text.primary}
lineHeight="1.8"
>
{stock.relation_desc?.data?.map(item => item.sentences || item.query_part).filter(Boolean).join('')}
</Text>
</Collapse>
</Box>
</Tooltip>
) : (
// 降级:纯文本版本(保留展开/收起功能)
<Tooltip
label={isDescExpanded ? "点击收起" : "点击展开完整描述"}
placement="top"
hasArrow
bg="rgba(20, 20, 20, 0.95)"
color={PROFESSIONAL_COLORS.gold[500]}
fontSize="xs"
>
<Box
onClick={(e) => {
e.stopPropagation();
setIsDescExpanded(!isDescExpanded);
}}
cursor="pointer"
bg={PROFESSIONAL_COLORS.background.secondary}
borderRadius="md"
_hover={{
bg: PROFESSIONAL_COLORS.background.cardHover,
}}
transition="background 0.2s"
position="relative"
>
{/* 去掉"关联描述"标题 */}
<Collapse in={isDescExpanded} startingHeight={56}>
<Text
fontSize="xs"
color={nameColor}
lineHeight="1.5"
>
{relationText}
</Text>
</Collapse>
{/* 提示信息 */}
{isDescExpanded && (
<Text
fontSize="xs"
color="gray.500"
mt={2}
fontStyle="italic"
>
AI生成仅供参考
</Text>
)}
</Box>
</Tooltip>
)}
</Box>
)}
</HStack>
</Box>
{/* 分时图弹窗 */}
{isTimelineModalOpen && (
<TimelineChartModal
isOpen={isTimelineModalOpen}
onClose={() => setIsTimelineModalOpen(false)}
stock={stock}
eventTime={eventTime}
/>
)}
{/* K线图弹窗 */}
{isKLineModalOpen && (
<KLineChartModal
isOpen={isKLineModalOpen}
onClose={() => setIsKLineModalOpen(false)}
stock={stock}
eventTime={eventTime}
/>
)}
</>
);
};
export default StockListItem;