// 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 ( {/* 右上角:重要性印章(放在卡片外层) */} {/* 事件卡片 */} onEventClick?.(event)} > {/* 时间标签 - 在卡片上方,宽度自适应,左对齐 */} {moment(event.created_at).format('YYYY-MM-DD HH:mm')} {periodLabel && ( <> {' • '} {periodLabel} )} {/* 右上角:关注按钮 */} onToggleFollow?.(event.id)} size="xs" showCount={false} /> {/* 标题 - 固定两行高度,保持卡片高度一致 */} onTitleClick?.(e, event)} mt={1} paddingRight="10px" minHeight="2.8em" display="flex" alignItems="center" > {event.title} {/* 第二行:涨跌幅数据 */} ); }; export default DynamicNewsEventCard;