323 lines
13 KiB
JavaScript
323 lines
13 KiB
JavaScript
// 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;
|