415 lines
13 KiB
JavaScript
415 lines
13 KiB
JavaScript
// 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 导入)
|
||
|
||
// 获取涨跌幅数据(优先使用 quote,fallback 到 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;
|