285 lines
8.8 KiB
JavaScript
285 lines
8.8 KiB
JavaScript
// src/views/Community/components/DynamicNewsDetail/StockListItem.js
|
||
// 股票卡片组件(融合表格功能的卡片样式)
|
||
|
||
import React, { useState } from 'react';
|
||
import {
|
||
Box,
|
||
Flex,
|
||
VStack,
|
||
SimpleGrid,
|
||
Text,
|
||
Button,
|
||
IconButton,
|
||
Collapse,
|
||
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';
|
||
|
||
/**
|
||
* 股票卡片组件
|
||
* @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)}%`;
|
||
};
|
||
|
||
// 获取涨跌幅颜色
|
||
const getChangeColor = (value) => {
|
||
const num = parseFloat(value);
|
||
if (isNaN(num) || num === 0) return 'gray.500';
|
||
return num > 0 ? 'red.500' : 'green.500';
|
||
};
|
||
|
||
// 获取涨跌幅数据(优先使用 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="md"
|
||
p={4}
|
||
onClick={handleViewDetail}
|
||
cursor="pointer"
|
||
_hover={{
|
||
boxShadow: 'md',
|
||
borderColor: 'blue.300',
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
<VStack align="stretch" spacing={3}>
|
||
{/* 顶部:股票代码 + 名称 + 操作按钮(上下两行布局) */}
|
||
<VStack align="stretch" spacing={2}>
|
||
{/* 第一行:股票代码 + 涨跌幅 + 操作按钮 */}
|
||
<Flex justify="space-between" align="center">
|
||
{/* 左侧:代码 + 涨跌幅 */}
|
||
<Flex align="baseline" gap={2}>
|
||
<Text
|
||
fontSize="md"
|
||
fontWeight="bold"
|
||
color={codeColor}
|
||
cursor="pointer"
|
||
onClick={handleViewDetail}
|
||
_hover={{ textDecoration: 'underline' }}
|
||
>
|
||
{stock.stock_code}
|
||
</Text>
|
||
<Text
|
||
fontSize="sm"
|
||
fontWeight="semibold"
|
||
color={getChangeColor(change)}
|
||
>
|
||
{formatChange(change)}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{/* 右侧:操作按钮 */}
|
||
<Flex gap={2}>
|
||
{onWatchlistToggle && (
|
||
<IconButton
|
||
size="sm"
|
||
variant={isInWatchlist ? 'solid' : 'outline'}
|
||
colorScheme={isInWatchlist ? 'yellow' : 'gray'}
|
||
icon={<StarIcon />}
|
||
onClick={handleWatchlistClick}
|
||
aria-label={isInWatchlist ? '已关注' : '加自选'}
|
||
title={isInWatchlist ? '已关注' : '加自选'}
|
||
/>
|
||
)}
|
||
<Button
|
||
size="sm"
|
||
colorScheme="blue"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleViewDetail();
|
||
}}
|
||
display={{ base: 'inline-flex', lg: 'none' }}
|
||
>
|
||
查看
|
||
</Button>
|
||
</Flex>
|
||
</Flex>
|
||
|
||
{/* 第二行:公司名称(彩色高亮) */}
|
||
<Text
|
||
fontSize="sm"
|
||
fontWeight="medium"
|
||
color={codeColor}
|
||
bg={useColorModeValue('blue.50', 'blue.900')}
|
||
px={2}
|
||
py={0.5}
|
||
borderRadius="sm"
|
||
width="fit-content"
|
||
>
|
||
{stock.stock_name}
|
||
</Text>
|
||
</VStack>
|
||
|
||
{/* 分隔线 */}
|
||
<Box borderTop="1px solid" borderColor={dividerColor} />
|
||
|
||
{/* 分时图 & K线图 - 左右布局 */}
|
||
<Box>
|
||
<SimpleGrid columns={2} spacing={3}>
|
||
{/* 左侧:分时图 */}
|
||
<Box onClick={(e) => e.stopPropagation()}>
|
||
<Text fontSize="xs" color={descColor} mb={1} textAlign="center">
|
||
分时图
|
||
</Text>
|
||
<MiniTimelineChart
|
||
stockCode={stock.stock_code}
|
||
eventTime={eventTime}
|
||
onClick={() => setIsModalOpen(true)}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 右侧:K线图 */}
|
||
<Box onClick={(e) => e.stopPropagation()}>
|
||
<Text fontSize="xs" color={descColor} mb={1} textAlign="center">
|
||
日K线
|
||
</Text>
|
||
<MiniKLineChart
|
||
stockCode={stock.stock_code}
|
||
eventTime={eventTime}
|
||
onClick={() => setIsModalOpen(true)}
|
||
/>
|
||
</Box>
|
||
</SimpleGrid>
|
||
</Box>
|
||
|
||
{/* 分隔线 */}
|
||
<Box borderTop="1px solid" borderColor={dividerColor} />
|
||
|
||
{/* 关联描述 */}
|
||
{relationText && relationText !== '--' && (
|
||
<Box>
|
||
<Text fontSize="xs" color={descColor} mb={1}>
|
||
关联描述:
|
||
</Text>
|
||
<Collapse in={isDescExpanded} startingHeight={40}>
|
||
<Text
|
||
fontSize="sm"
|
||
color={nameColor}
|
||
lineHeight="1.6"
|
||
cursor={needTruncate ? "pointer" : "default"}
|
||
onClick={(e) => {
|
||
if (needTruncate) {
|
||
e.stopPropagation();
|
||
setIsDescExpanded(!isDescExpanded);
|
||
}
|
||
}}
|
||
_hover={needTruncate ? { opacity: 0.8 } : {}}
|
||
>
|
||
{relationText}
|
||
</Text>
|
||
</Collapse>
|
||
{needTruncate && (
|
||
<Button
|
||
size="xs"
|
||
variant="link"
|
||
colorScheme="blue"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setIsDescExpanded(!isDescExpanded);
|
||
}}
|
||
mt={1}
|
||
>
|
||
{isDescExpanded ? '收起' : '展开'}
|
||
</Button>
|
||
)}
|
||
|
||
{/* 合规提示 */}
|
||
<Text
|
||
fontSize="xs"
|
||
color="gray.500"
|
||
mt={2}
|
||
fontStyle="italic"
|
||
>
|
||
⚠️ 以上关联描述由AI生成,仅供参考,不构成投资建议
|
||
</Text>
|
||
</Box>
|
||
)}
|
||
</VStack>
|
||
</Box>
|
||
|
||
{/* 股票详情弹窗 */}
|
||
<StockChartModal
|
||
isOpen={isModalOpen}
|
||
onClose={() => setIsModalOpen(false)}
|
||
stock={stock}
|
||
eventTime={eventTime}
|
||
size="6xl"
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default StockListItem;
|