Files
vf_react/src/views/Community/components/EventCard/DynamicNewsEventCard.js
2025-11-07 11:08:06 +08:00

323 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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/EventCard/DynamicNewsEventCard.js
// 动态新闻事件卡片组件(纵向布局,时间在上)
import React from 'react';
import {
VStack,
Card,
CardBody,
Box,
Text,
Tooltip,
useColorModeValue,
} from '@chakra-ui/react';
import moment from 'moment';
import { getImportanceConfig } from '../../../../constants/importanceLevels';
import { getChangeColor } from '../../../../utils/colorUtils';
// 导入子组件
import ImportanceStamp from './ImportanceStamp';
import EventFollowButton from './EventFollowButton';
import StockChangeIndicators from '../../../../components/StockChangeIndicators';
/**
* 动态新闻事件卡片组件(极简版)
* @param {Object} props
* @param {Object} props.event - 事件对象
* @param {number} props.index - 事件索引
* @param {boolean} props.isFollowing - 是否已关注
* @param {number} props.followerCount - 关注数
* @param {boolean} props.isSelected - 是否被选中
* @param {Function} props.onEventClick - 卡片点击事件
* @param {Function} props.onTitleClick - 标题点击事件
* @param {Function} props.onToggleFollow - 切换关注事件
* @param {string} props.borderColor - 边框颜色
*/
const DynamicNewsEventCard = ({
event,
index,
isFollowing,
followerCount,
isSelected = false,
onEventClick,
onTitleClick,
onToggleFollow,
borderColor,
}) => {
const cardBg = useColorModeValue('white', 'gray.800');
const linkColor = useColorModeValue('blue.600', 'blue.400');
const importance = getImportanceConfig(event.importance);
/**
* 判断交易时段(盘前、盘中上午、午休、盘中下午、盘后)
* @param {string} timestamp - 事件时间戳
* @returns {'pre-market' | 'morning-trading' | 'lunch-break' | 'afternoon-trading' | 'after-market'}
*/
const getTradingPeriod = (timestamp) => {
const eventTime = moment(timestamp);
const hour = eventTime.hour();
const minute = eventTime.minute();
const timeInMinutes = hour * 60 + minute;
// 时间常量
const morningStart = 9 * 60 + 30; // 09:30 = 570分钟
const morningEnd = 11 * 60 + 30; // 11:30 = 690分钟
const lunchEnd = 13 * 60; // 13:00 = 780分钟
const afternoonEnd = 15 * 60; // 15:00 = 900分钟
// 盘中上午09:30-11:30
if (timeInMinutes >= morningStart && timeInMinutes < morningEnd) {
return 'morning-trading';
}
// 午休11:30-13:00
else if (timeInMinutes >= morningEnd && timeInMinutes < lunchEnd) {
return 'lunch-break';
}
// 盘中下午13:00-15:00
else if (timeInMinutes >= lunchEnd && timeInMinutes < afternoonEnd) {
return 'afternoon-trading';
}
// 盘前00:00-09:30
else if (timeInMinutes < morningStart) {
return 'pre-market';
}
// 盘后15:00-23:59
else {
return 'after-market';
}
};
/**
* 获取时间标签样式(根据交易时段)
* @param {string} period - 交易时段
* @returns {Object} Chakra UI 样式对象
*/
const getTimeLabelStyle = (period) => {
switch (period) {
case 'pre-market':
// 盘前:粉红色系(浅红)
return {
bg: useColorModeValue('pink.50', 'pink.900'),
borderColor: useColorModeValue('pink.300', 'pink.500'),
textColor: useColorModeValue('pink.600', 'pink.300'),
};
case 'morning-trading':
case 'afternoon-trading':
// 盘中:红色系(强烈,表示交易活跃)
return {
bg: useColorModeValue('red.50', 'red.900'),
borderColor: useColorModeValue('red.400', 'red.500'),
textColor: useColorModeValue('red.700', 'red.300'),
};
case 'lunch-break':
// 午休:灰色系(中性)
return {
bg: useColorModeValue('gray.100', 'gray.800'),
borderColor: useColorModeValue('gray.400', 'gray.500'),
textColor: useColorModeValue('gray.600', 'gray.400'),
};
case 'after-market':
// 盘后:橙色系(暖色但区别于盘中红色)
return {
bg: useColorModeValue('orange.50', 'orange.900'),
borderColor: useColorModeValue('orange.400', 'orange.500'),
textColor: useColorModeValue('orange.600', 'orange.300'),
};
default:
return {
bg: useColorModeValue('gray.100', 'gray.800'),
borderColor: useColorModeValue('gray.400', 'gray.500'),
textColor: useColorModeValue('gray.600', 'gray.400'),
};
}
};
/**
* 获取交易时段文字标签
* @param {string} period - 交易时段
* @returns {string} 时段文字标签
*/
const getPeriodLabel = (period) => {
switch (period) {
case 'pre-market':
return '盘前';
case 'morning-trading':
case 'afternoon-trading':
return '盘中';
case 'lunch-break':
return '午休';
case 'after-market':
return '盘后';
default:
return '';
}
};
/**
* 根据平均涨幅计算背景色(分级策略)- 使用毛玻璃效果
* @param {number} avgChange - 平均涨跌幅
* @returns {string} Chakra UI 颜色值
*/
const getChangeBasedBgColor = (avgChange) => {
const numChange = Number(avgChange);
// 如果没有涨跌幅数据,使用半透明背景
if (avgChange == null || isNaN(numChange)) {
return useColorModeValue('rgba(255, 255, 255, 0.8)', 'rgba(26, 32, 44, 0.8)');
}
// 根据涨跌幅分级返回半透明背景色(毛玻璃效果)
const absChange = Math.abs(numChange);
if (numChange > 0) {
// 涨:红色系半透明
if (absChange >= 9) return useColorModeValue('rgba(254, 202, 202, 0.9)', 'rgba(127, 29, 29, 0.9)');
if (absChange >= 7) return useColorModeValue('rgba(254, 202, 202, 0.8)', 'rgba(153, 27, 27, 0.8)');
if (absChange >= 5) return useColorModeValue('rgba(254, 226, 226, 0.8)', 'rgba(185, 28, 28, 0.8)');
if (absChange >= 3) return useColorModeValue('rgba(254, 226, 226, 0.7)', 'rgba(220, 38, 38, 0.7)');
return useColorModeValue('rgba(254, 242, 242, 0.7)', 'rgba(239, 68, 68, 0.7)');
} else if (numChange < 0) {
// 跌:绿色系半透明
if (absChange >= 9) return useColorModeValue('rgba(187, 247, 208, 0.9)', 'rgba(20, 83, 45, 0.9)');
if (absChange >= 7) return useColorModeValue('rgba(187, 247, 208, 0.8)', 'rgba(22, 101, 52, 0.8)');
if (absChange >= 5) return useColorModeValue('rgba(209, 250, 229, 0.8)', 'rgba(21, 128, 61, 0.8)');
if (absChange >= 3) return useColorModeValue('rgba(209, 250, 229, 0.7)', 'rgba(22, 163, 74, 0.7)');
return useColorModeValue('rgba(240, 253, 244, 0.7)', 'rgba(34, 197, 94, 0.7)');
}
return useColorModeValue('rgba(255, 255, 255, 0.8)', 'rgba(26, 32, 44, 0.8)');
};
// 获取当前事件的交易时段、样式和文字标签
const tradingPeriod = getTradingPeriod(event.created_at);
const timeLabelStyle = getTimeLabelStyle(tradingPeriod);
const periodLabel = getPeriodLabel(tradingPeriod);
return (
<VStack align="stretch" spacing={2} w="100%" pt={8} position="relative">
{/* 右上角:重要性印章(放在卡片外层) */}
<Box position="absolute" top={-4} right={4} zIndex={10}>
<ImportanceStamp importance={event.importance} />
</Box>
{/* 事件卡片 */}
<Card
position="relative"
bg={isSelected
? useColorModeValue('blue.50', 'blue.900')
: getChangeBasedBgColor(event.related_avg_chg)
}
backdropFilter="blur(10px)" // 毛玻璃效果
borderWidth={isSelected ? "2px" : "1px"}
borderColor={isSelected
? useColorModeValue('blue.500', 'blue.400')
: borderColor
}
borderRadius="lg"
boxShadow={isSelected ? "xl" : "md"}
overflow="visible"
_hover={{
boxShadow: '2xl',
transform: 'translateY(-4px)',
borderColor: isSelected ? 'blue.600' : importance.color,
}}
transition="all 0.3s ease"
cursor="pointer"
onClick={() => onEventClick?.(event)}
>
<CardBody p={3}>
{/* 时间标签 - 在卡片上方,宽度自适应,左对齐 */}
<Box
bg={timeLabelStyle.bg}
borderWidth="2px"
borderColor={timeLabelStyle.borderColor}
borderRadius="md"
px={0.5}
py={0.5}
width="fit-content"
alignSelf="flex-start"
boxShadow="sm"
transition="all 0.3s ease"
position="absolute"
top={-2}
left={8}
>
<Text
fontSize="xs"
fontWeight="bold"
color={timeLabelStyle.textColor}
lineHeight="1.3"
>
{moment(event.created_at).format('YYYY-MM-DD HH:mm')}
{periodLabel && (
<>
{' • '}
<Text as="span" fontWeight="extrabold">
{periodLabel}
</Text>
</>
)}
</Text>
</Box>
{/* 右上角:关注按钮 */}
<Box position="absolute" top={2} right={2} zIndex={1}>
<EventFollowButton
isFollowing={isFollowing}
followerCount={followerCount}
onToggle={() => onToggleFollow?.(event.id)}
size="xs"
showCount={false}
/>
</Box>
<VStack align="stretch" spacing={2}>
{/* 标题 - 固定两行高度,保持卡片高度一致 */}
<Tooltip
label={event.title}
placement="top"
hasArrow
bg="gray.700"
color="white"
fontSize="sm"
p={2}
borderRadius="md"
>
<Box
cursor="pointer"
onClick={(e) => onTitleClick?.(e, event)}
mt={1}
paddingRight="10px"
minHeight="2.8em"
display="flex"
alignItems="center"
>
<Text
fontSize="md"
fontWeight="semibold"
color={linkColor}
lineHeight="1.4"
noOfLines={2}
_hover={{ textDecoration: 'underline' }}
>
{event.title}
</Text>
</Box>
</Tooltip>
<Box position='relative'>
{/* 第二行:涨跌幅数据 */}
<StockChangeIndicators
avgChange={event.related_avg_chg}
maxChange={event.related_max_chg}
weekChange={event.related_week_chg}
/>
</Box>
</VStack>
</CardBody>
</Card>
</VStack>
);
};
export default DynamicNewsEventCard;