feat: 新增实时要闻·动态追踪与市场复盘功能,优化导航体验
新增功能: - 实时要闻·动态追踪横向滚动卡片(DynamicNewsCard) - 动态新闻事件卡片组件(DynamicNewsEventCard) - 市场复盘卡片组件(MarketReviewCard) - 股票涨跌幅指标组件(StockChangeIndicators) - 交易时间工具函数(tradingTimeUtils) - Mock API 支持动态新闻数据生成 UI 优化: - EventFollowButton 改用 react-icons 星星图标,实现真正的空心/实心效果 - 关注按钮添加半透明白色背景(whiteAlpha.500),悬停效果更明显 - 事件卡片标题添加右侧留白,防止关注按钮遮挡文字 性能优化: - 禁用 Router v7_startTransition 特性,解决路由切换延迟 2 秒问题 - 调整导航菜单点击顺序(先跳转后关闭),提升响应速度 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
239
src/views/Community/components/DynamicNewsCard.js
Normal file
239
src/views/Community/components/DynamicNewsCard.js
Normal file
@@ -0,0 +1,239 @@
|
||||
// src/views/Community/components/DynamicNewsCard.js
|
||||
// 横向滚动事件卡片组件(实时要闻·动态追踪)
|
||||
|
||||
import React, { forwardRef, useRef, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Box,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Text,
|
||||
Badge,
|
||||
IconButton,
|
||||
Center,
|
||||
Spinner,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronLeftIcon, ChevronRightIcon, TimeIcon } from '@chakra-ui/icons';
|
||||
import DynamicNewsEventCard from './EventCard/DynamicNewsEventCard';
|
||||
|
||||
/**
|
||||
* 实时要闻·动态追踪 - 横向滚动卡片组件
|
||||
* @param {Array} events - 事件列表
|
||||
* @param {boolean} loading - 加载状态
|
||||
* @param {Date} lastUpdateTime - 最后更新时间
|
||||
* @param {Function} onEventClick - 事件点击回调
|
||||
* @param {Function} onViewDetail - 查看详情回调
|
||||
* @param {Object} ref - 用于滚动的ref
|
||||
*/
|
||||
const DynamicNewsCard = forwardRef(({
|
||||
events,
|
||||
loading,
|
||||
lastUpdateTime,
|
||||
onEventClick,
|
||||
onViewDetail,
|
||||
...rest
|
||||
}, ref) => {
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const scrollContainerRef = useRef(null);
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(true);
|
||||
|
||||
// 滚动到左侧
|
||||
const scrollLeft = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollBy({
|
||||
left: -400,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 滚动到右侧
|
||||
const scrollRight = () => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollBy({
|
||||
left: 400,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 监听滚动位置,更新箭头显示状态
|
||||
const handleScroll = (e) => {
|
||||
const container = e.target;
|
||||
const scrollLeft = container.scrollLeft;
|
||||
const scrollWidth = container.scrollWidth;
|
||||
const clientWidth = container.clientWidth;
|
||||
|
||||
setShowLeftArrow(scrollLeft > 0);
|
||||
setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 10);
|
||||
};
|
||||
|
||||
// 时间轴样式配置
|
||||
const getTimelineBoxStyle = () => {
|
||||
return {
|
||||
bg: useColorModeValue('gray.50', 'gray.700'),
|
||||
borderColor: useColorModeValue('gray.400', 'gray.500'),
|
||||
borderWidth: '2px',
|
||||
textColor: useColorModeValue('blue.600', 'blue.400'),
|
||||
boxShadow: 'sm',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Card ref={ref} {...rest} bg={cardBg} borderColor={borderColor} mb={4}>
|
||||
{/* 标题部分 */}
|
||||
<CardHeader>
|
||||
<Flex justify="space-between" align="center">
|
||||
<VStack align="start" spacing={1}>
|
||||
<Heading size="md">
|
||||
<HStack>
|
||||
<TimeIcon />
|
||||
<Text>实时要闻·动态追踪</Text>
|
||||
</HStack>
|
||||
</Heading>
|
||||
<HStack fontSize="sm" color="gray.500">
|
||||
<Badge colorScheme="red">实时</Badge>
|
||||
<Badge colorScheme="green">盘中</Badge>
|
||||
<Badge colorScheme="blue">快讯</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'}
|
||||
</Text>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
{/* 主体内容 */}
|
||||
<CardBody position="relative">
|
||||
{/* Loading 状态 */}
|
||||
{loading && (
|
||||
<Center py={10}>
|
||||
<VStack>
|
||||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||||
<Text color="gray.500">正在加载最新事件...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* Empty 状态 */}
|
||||
{!loading && (!events || events.length === 0) && (
|
||||
<Center py={10}>
|
||||
<VStack>
|
||||
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* 横向滚动事件列表 */}
|
||||
{!loading && events && events.length > 0 && (
|
||||
<Box position="relative">
|
||||
{/* 左侧滚动按钮 */}
|
||||
{showLeftArrow && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon boxSize={6} />}
|
||||
position="absolute"
|
||||
left="-4"
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
zIndex={2}
|
||||
onClick={scrollLeft}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
size="md"
|
||||
borderRadius="full"
|
||||
shadow="md"
|
||||
aria-label="向左滚动"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 右侧滚动按钮 */}
|
||||
{showRightArrow && (
|
||||
<IconButton
|
||||
icon={<ChevronRightIcon boxSize={6} />}
|
||||
position="absolute"
|
||||
right="-4"
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
zIndex={2}
|
||||
onClick={scrollRight}
|
||||
colorScheme="blue"
|
||||
variant="solid"
|
||||
size="md"
|
||||
borderRadius="full"
|
||||
shadow="md"
|
||||
aria-label="向右滚动"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 横向滚动容器 */}
|
||||
<Flex
|
||||
ref={scrollContainerRef}
|
||||
overflowX="auto"
|
||||
overflowY="hidden"
|
||||
gap={4}
|
||||
py={4}
|
||||
px={2}
|
||||
onScroll={handleScroll}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
height: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: useColorModeValue('#f1f1f1', '#2D3748'),
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: useColorModeValue('#888', '#4A5568'),
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: useColorModeValue('#555', '#718096'),
|
||||
},
|
||||
// 平滑滚动
|
||||
scrollBehavior: 'smooth',
|
||||
// 触摸设备优化
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{events.map((event, index) => (
|
||||
<Box
|
||||
key={event.id}
|
||||
minW="calc((100% - 64px) / 5)"
|
||||
maxW="calc((100% - 64px) / 5)"
|
||||
flexShrink={0}
|
||||
>
|
||||
<DynamicNewsEventCard
|
||||
event={event}
|
||||
index={index}
|
||||
isFollowing={false}
|
||||
followerCount={event.follower_count || 0}
|
||||
onEventClick={onEventClick}
|
||||
onTitleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onEventClick(event);
|
||||
}}
|
||||
onToggleFollow={() => {}}
|
||||
timelineStyle={getTimelineBoxStyle()}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
DynamicNewsCard.displayName = 'DynamicNewsCard';
|
||||
|
||||
export default DynamicNewsCard;
|
||||
141
src/views/Community/components/EventCard/DynamicNewsEventCard.js
Normal file
141
src/views/Community/components/EventCard/DynamicNewsEventCard.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// src/views/Community/components/EventCard/DynamicNewsEventCard.js
|
||||
// 动态新闻事件卡片组件(纵向布局,时间在上)
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Card,
|
||||
CardBody,
|
||||
Box,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import moment from 'moment';
|
||||
import { getImportanceConfig } from '../../../../constants/importanceLevels';
|
||||
|
||||
// 导入子组件
|
||||
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 {Function} props.onEventClick - 卡片点击事件
|
||||
* @param {Function} props.onTitleClick - 标题点击事件
|
||||
* @param {Function} props.onToggleFollow - 切换关注事件
|
||||
* @param {Object} props.timelineStyle - 时间轴样式配置
|
||||
* @param {string} props.borderColor - 边框颜色
|
||||
*/
|
||||
const DynamicNewsEventCard = ({
|
||||
event,
|
||||
index,
|
||||
isFollowing,
|
||||
followerCount,
|
||||
onEventClick,
|
||||
onTitleClick,
|
||||
onToggleFollow,
|
||||
timelineStyle,
|
||||
borderColor,
|
||||
}) => {
|
||||
const importance = getImportanceConfig(event.importance);
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const linkColor = useColorModeValue('blue.600', 'blue.400');
|
||||
|
||||
return (
|
||||
<VStack align="stretch" spacing={2} w="full">
|
||||
{/* 时间标签 - 在卡片上方 */}
|
||||
<Box
|
||||
{...(timelineStyle.bgGradient ? { bgGradient: timelineStyle.bgGradient } : { bg: timelineStyle.bg })}
|
||||
borderWidth={timelineStyle.borderWidth}
|
||||
borderColor={timelineStyle.borderColor}
|
||||
borderRadius="md"
|
||||
px={3}
|
||||
py={1.5}
|
||||
textAlign="center"
|
||||
boxShadow={timelineStyle.boxShadow}
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color={timelineStyle.textColor}
|
||||
lineHeight="1.3"
|
||||
>
|
||||
{moment(event.created_at).format('YYYY-MM-DD HH:mm')}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 事件卡片 */}
|
||||
<Card
|
||||
position="relative"
|
||||
bg={index % 2 === 0 ? cardBg : useColorModeValue('gray.50', 'gray.750')}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
boxShadow="sm"
|
||||
_hover={{
|
||||
boxShadow: 'lg',
|
||||
transform: 'translateY(-2px)',
|
||||
borderColor: importance.color,
|
||||
}}
|
||||
transition="all 0.3s ease"
|
||||
cursor="pointer"
|
||||
onClick={() => onEventClick?.(event)}
|
||||
>
|
||||
<CardBody p={3}>
|
||||
{/* 关注按钮 - 绝对定位在右上角 */}
|
||||
<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.5}>
|
||||
{/* 第一行:标题 + 重要性(行内文字) */}
|
||||
<Box
|
||||
cursor="pointer"
|
||||
onClick={(e) => onTitleClick?.(e, event)}
|
||||
paddingRight="10px"
|
||||
>
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="semibold"
|
||||
color={linkColor}
|
||||
lineHeight="1.4"
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
{event.title}
|
||||
<Text
|
||||
as="span"
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={importance.color}
|
||||
ml={2}
|
||||
>
|
||||
[{importance.level}]
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 第二行:涨跌幅数据 */}
|
||||
<StockChangeIndicators
|
||||
avgChange={event.related_avg_chg}
|
||||
maxChange={event.related_max_chg}
|
||||
weekChange={event.related_week_chg}
|
||||
/>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicNewsEventCard;
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Community/components/EventCard/EventFollowButton.js
|
||||
import React from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { StarIcon } from '@chakra-ui/icons';
|
||||
import { IconButton, Box } from '@chakra-ui/react';
|
||||
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||
|
||||
/**
|
||||
* 事件关注按钮组件
|
||||
@@ -19,7 +19,7 @@ const EventFollowButton = ({
|
||||
size = 'sm',
|
||||
showCount = true
|
||||
}) => {
|
||||
const iconSize = size === 'xs' ? '10px' : '12px';
|
||||
const iconSize = size === 'xs' ? '16px' : size === 'sm' ? '18px' : '22px';
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -27,16 +27,38 @@ const EventFollowButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
colorScheme="yellow"
|
||||
variant={isFollowing ? 'solid' : 'outline'}
|
||||
leftIcon={<StarIcon boxSize={iconSize} />}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isFollowing ? '已关注' : '关注'}
|
||||
{showCount && followerCount > 0 && `(${followerCount})`}
|
||||
</Button>
|
||||
<Box display="inline-flex" alignItems="center" gap={1}>
|
||||
<IconButton
|
||||
size={size}
|
||||
colorScheme="yellow"
|
||||
variant="ghost"
|
||||
bg="whiteAlpha.500"
|
||||
boxShadow="sm"
|
||||
_hover={{
|
||||
bg: 'whiteAlpha.800',
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
icon={
|
||||
isFollowing ? (
|
||||
<AiFillStar
|
||||
size={iconSize}
|
||||
color="gold"
|
||||
/>
|
||||
) : (
|
||||
<AiOutlineStar
|
||||
size={iconSize}
|
||||
color="#718096"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handleClick}
|
||||
aria-label={isFollowing ? '取消关注' : '关注'}
|
||||
/>
|
||||
{/* <Box fontSize="xs" color="gray.500">
|
||||
{followerCount || 0}
|
||||
</Box> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
300
src/views/Community/components/MarketReviewCard.js
Normal file
300
src/views/Community/components/MarketReviewCard.js
Normal file
@@ -0,0 +1,300 @@
|
||||
// src/views/Community/components/MarketReviewCard.js
|
||||
// 市场复盘组件(左右布局:事件列表 | 事件详情)
|
||||
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Box,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Text,
|
||||
Badge,
|
||||
Center,
|
||||
Spinner,
|
||||
useColorModeValue,
|
||||
Grid,
|
||||
GridItem,
|
||||
} from '@chakra-ui/react';
|
||||
import { TimeIcon, InfoIcon } from '@chakra-ui/icons';
|
||||
import moment from 'moment';
|
||||
import CompactEventCard from './EventCard/CompactEventCard';
|
||||
import EventHeader from './EventCard/EventHeader';
|
||||
import EventStats from './EventCard/EventStats';
|
||||
import EventFollowButton from './EventCard/EventFollowButton';
|
||||
import EventPriceDisplay from './EventCard/EventPriceDisplay';
|
||||
import EventDescription from './EventCard/EventDescription';
|
||||
import { getImportanceConfig } from '../../../constants/importanceLevels';
|
||||
|
||||
/**
|
||||
* 市场复盘 - 左右布局卡片组件
|
||||
* @param {Array} events - 事件列表
|
||||
* @param {boolean} loading - 加载状态
|
||||
* @param {Date} lastUpdateTime - 最后更新时间
|
||||
* @param {Function} onEventClick - 事件点击回调
|
||||
* @param {Function} onViewDetail - 查看详情回调
|
||||
* @param {Function} onToggleFollow - 切换关注回调
|
||||
* @param {Object} ref - 用于滚动的ref
|
||||
*/
|
||||
const MarketReviewCard = forwardRef(({
|
||||
events,
|
||||
loading,
|
||||
lastUpdateTime,
|
||||
onEventClick,
|
||||
onViewDetail,
|
||||
onToggleFollow,
|
||||
...rest
|
||||
}, ref) => {
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const linkColor = useColorModeValue('blue.600', 'blue.400');
|
||||
const mutedColor = useColorModeValue('gray.500', 'gray.400');
|
||||
const textColor = useColorModeValue('gray.700', 'gray.200');
|
||||
const selectedBg = useColorModeValue('blue.50', 'blue.900');
|
||||
|
||||
// 选中的事件
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
|
||||
// 时间轴样式配置
|
||||
const getTimelineBoxStyle = () => {
|
||||
return {
|
||||
bg: useColorModeValue('gray.50', 'gray.700'),
|
||||
borderColor: useColorModeValue('gray.400', 'gray.500'),
|
||||
borderWidth: '2px',
|
||||
textColor: useColorModeValue('blue.600', 'blue.400'),
|
||||
boxShadow: 'sm',
|
||||
};
|
||||
};
|
||||
|
||||
// 处理事件点击
|
||||
const handleEventClick = (event) => {
|
||||
setSelectedEvent(event);
|
||||
if (onEventClick) {
|
||||
onEventClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染右侧事件详情
|
||||
const renderEventDetail = () => {
|
||||
if (!selectedEvent) {
|
||||
return (
|
||||
<Center h="full" minH="400px">
|
||||
<VStack spacing={4}>
|
||||
<InfoIcon boxSize={12} color={mutedColor} />
|
||||
<Text color={mutedColor} fontSize="lg">
|
||||
请从左侧选择事件查看详情
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const importance = getImportanceConfig(selectedEvent.importance);
|
||||
|
||||
return (
|
||||
<Card
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
boxShadow="md"
|
||||
h="full"
|
||||
>
|
||||
<CardBody p={6}>
|
||||
<VStack align="stretch" spacing={4}>
|
||||
{/* 第一行:标题+优先级 | 统计+关注 */}
|
||||
<Flex align="center" justify="space-between" gap={3}>
|
||||
{/* 左侧:标题 + 优先级标签 */}
|
||||
<EventHeader
|
||||
title={selectedEvent.title}
|
||||
importance={selectedEvent.importance}
|
||||
onTitleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onViewDetail) {
|
||||
onViewDetail(e, selectedEvent.id);
|
||||
}
|
||||
}}
|
||||
linkColor={linkColor}
|
||||
compact={false}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{/* 右侧:统计数据 + 关注按钮 */}
|
||||
<HStack spacing={4} flexShrink={0}>
|
||||
{/* 统计数据 */}
|
||||
<EventStats
|
||||
viewCount={selectedEvent.view_count}
|
||||
postCount={selectedEvent.post_count}
|
||||
followerCount={selectedEvent.follower_count}
|
||||
size="md"
|
||||
spacing={4}
|
||||
display="flex"
|
||||
mutedColor={mutedColor}
|
||||
/>
|
||||
|
||||
{/* 关注按钮 */}
|
||||
<EventFollowButton
|
||||
isFollowing={false}
|
||||
followerCount={selectedEvent.follower_count}
|
||||
onToggle={() => onToggleFollow && onToggleFollow(selectedEvent.id)}
|
||||
size="sm"
|
||||
showCount={false}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 第二行:价格标签 | 时间+作者 */}
|
||||
<Flex justify="space-between" align="center" wrap="wrap" gap={3}>
|
||||
{/* 左侧:价格标签 */}
|
||||
<EventPriceDisplay
|
||||
avgChange={selectedEvent.related_avg_chg}
|
||||
maxChange={selectedEvent.related_max_chg}
|
||||
weekChange={selectedEvent.related_week_chg}
|
||||
compact={false}
|
||||
/>
|
||||
|
||||
{/* 右侧:时间 + 作者 */}
|
||||
<HStack spacing={2} fontSize="sm" flexShrink={0}>
|
||||
<Text fontWeight="bold" color={linkColor}>
|
||||
{moment(selectedEvent.created_at).format('YYYY-MM-DD HH:mm')}
|
||||
</Text>
|
||||
<Text color={mutedColor}>•</Text>
|
||||
<Text color={mutedColor}>@{selectedEvent.creator?.username || 'Anonymous'}</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 第三行:描述文字 */}
|
||||
<EventDescription
|
||||
description={selectedEvent.description}
|
||||
textColor={textColor}
|
||||
minLength={200}
|
||||
noOfLines={10}
|
||||
/>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card ref={ref} {...rest} bg={cardBg} borderColor={borderColor} mb={4}>
|
||||
{/* 标题部分 */}
|
||||
<CardHeader>
|
||||
<Flex justify="space-between" align="center">
|
||||
<VStack align="start" spacing={1}>
|
||||
<Heading size="md">
|
||||
<HStack>
|
||||
<TimeIcon />
|
||||
<Text>市场复盘</Text>
|
||||
</HStack>
|
||||
</Heading>
|
||||
<HStack fontSize="sm" color="gray.500">
|
||||
<Badge colorScheme="orange">复盘</Badge>
|
||||
<Badge colorScheme="purple">总结</Badge>
|
||||
<Badge colorScheme="gray">完整</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'}
|
||||
</Text>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
{/* 主体内容 */}
|
||||
<CardBody>
|
||||
{/* Loading 状态 */}
|
||||
{loading && (
|
||||
<Center py={10}>
|
||||
<VStack>
|
||||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||||
<Text color="gray.500">正在加载复盘数据...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* Empty 状态 */}
|
||||
{!loading && (!events || events.length === 0) && (
|
||||
<Center py={10}>
|
||||
<VStack>
|
||||
<Text fontSize="lg" color="gray.500">暂无复盘数据</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{/* 左右布局:事件列表 | 事件详情 */}
|
||||
{!loading && events && events.length > 0 && (
|
||||
<Grid templateColumns="1fr 2fr" gap={6} minH="500px">
|
||||
{/* 左侧:事件列表 (33.3%) */}
|
||||
<GridItem>
|
||||
<Box
|
||||
overflowY="auto"
|
||||
maxH="600px"
|
||||
pr={2}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '6px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: useColorModeValue('#f1f1f1', '#2D3748'),
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: useColorModeValue('#888', '#4A5568'),
|
||||
borderRadius: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: useColorModeValue('#555', '#718096'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{events.map((event, index) => (
|
||||
<Box
|
||||
key={event.id}
|
||||
onClick={() => handleEventClick(event)}
|
||||
cursor="pointer"
|
||||
bg={selectedEvent?.id === event.id ? selectedBg : 'transparent'}
|
||||
borderRadius="md"
|
||||
transition="all 0.2s"
|
||||
_hover={{ bg: selectedBg }}
|
||||
>
|
||||
<CompactEventCard
|
||||
event={event}
|
||||
index={index}
|
||||
isFollowing={false}
|
||||
followerCount={event.follower_count || 0}
|
||||
onEventClick={() => handleEventClick(event)}
|
||||
onTitleClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleEventClick(event);
|
||||
}}
|
||||
onViewDetail={onViewDetail}
|
||||
onToggleFollow={() => {}}
|
||||
timelineStyle={getTimelineBoxStyle()}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
</GridItem>
|
||||
|
||||
{/* 右侧:事件详情 (66.7%) */}
|
||||
<GridItem>
|
||||
{renderEventDetail()}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
MarketReviewCard.displayName = 'MarketReviewCard';
|
||||
|
||||
export default MarketReviewCard;
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/views/Community/index.js
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { fetchPopularKeywords, fetchHotEvents } from '../../store/slices/communityDataSlice';
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
|
||||
// 导入组件
|
||||
import EventTimelineCard from './components/EventTimelineCard';
|
||||
import DynamicNewsCard from './components/DynamicNewsCard';
|
||||
import MarketReviewCard from './components/MarketReviewCard';
|
||||
import HotEventsSection from './components/HotEventsSection';
|
||||
import EventModals from './components/EventModals';
|
||||
|
||||
@@ -19,6 +21,13 @@ import { useEventData } from './hooks/useEventData';
|
||||
import { useEventFilters } from './hooks/useEventFilters';
|
||||
import { useCommunityEvents } from './hooks/useCommunityEvents';
|
||||
|
||||
// 导入时间工具函数
|
||||
import {
|
||||
getCurrentTradingTimeRange,
|
||||
getMarketReviewTimeRange,
|
||||
filterEventsByTimeRange
|
||||
} from '../../utils/tradingTimeUtils';
|
||||
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
import { usePostHogTrack } from '../../hooks/usePostHogRedux';
|
||||
@@ -48,6 +57,10 @@ const Community = () => {
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
|
||||
|
||||
// 动态新闻数据状态
|
||||
const [dynamicNewsEvents, setDynamicNewsEvents] = useState([]);
|
||||
const [dynamicNewsLoading, setDynamicNewsLoading] = useState(true);
|
||||
|
||||
// 🎯 初始化Community埋点Hook
|
||||
const communityEvents = useCommunityEvents({ navigate });
|
||||
|
||||
@@ -60,12 +73,64 @@ const Community = () => {
|
||||
|
||||
const { events, pagination, loading, lastUpdateTime } = useEventData(filters);
|
||||
|
||||
// 计算市场复盘的时间范围和过滤后的事件
|
||||
const marketReviewData = useMemo(() => {
|
||||
const timeRange = getMarketReviewTimeRange();
|
||||
const filteredEvents = filterEventsByTimeRange(events, timeRange.startTime, timeRange.endTime);
|
||||
logger.debug('Community', '市场复盘时间范围', {
|
||||
description: timeRange.description,
|
||||
rangeType: timeRange.rangeType,
|
||||
eventCount: filteredEvents.length
|
||||
});
|
||||
return {
|
||||
events: filteredEvents,
|
||||
timeRange
|
||||
};
|
||||
}, [events]);
|
||||
|
||||
// 加载热门关键词和热点事件(使用Redux,内部有缓存判断)
|
||||
useEffect(() => {
|
||||
dispatch(fetchPopularKeywords());
|
||||
dispatch(fetchHotEvents());
|
||||
}, [dispatch]);
|
||||
|
||||
// 加载动态新闻数据
|
||||
useEffect(() => {
|
||||
const fetchDynamicNews = async () => {
|
||||
setDynamicNewsLoading(true);
|
||||
try {
|
||||
const timeRange = getCurrentTradingTimeRange();
|
||||
const response = await fetch(
|
||||
`/api/events/dynamic-news?start_time=${timeRange.startTime.toISOString()}&end_time=${timeRange.endTime.toISOString()}&count=30`,
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
setDynamicNewsEvents(data.data);
|
||||
logger.info('Community', '动态新闻加载成功', {
|
||||
count: data.data.length,
|
||||
timeRange: timeRange.description
|
||||
});
|
||||
} else {
|
||||
logger.warn('Community', '动态新闻加载失败', data);
|
||||
setDynamicNewsEvents([]);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Community', '动态新闻加载异常', error);
|
||||
setDynamicNewsEvents([]);
|
||||
} finally {
|
||||
setDynamicNewsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDynamicNews();
|
||||
|
||||
// 每5分钟刷新一次动态新闻
|
||||
const interval = setInterval(fetchDynamicNews, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 🎯 PostHog 追踪:页面浏览
|
||||
// useEffect(() => {
|
||||
// track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, {
|
||||
@@ -120,7 +185,28 @@ const Community = () => {
|
||||
{/* 热点事件区域 */}
|
||||
<HotEventsSection events={hotEvents} />
|
||||
|
||||
{/* 实时事件 */}
|
||||
{/* 实时要闻·动态追踪 - 横向滚动 */}
|
||||
<DynamicNewsCard
|
||||
mt={6}
|
||||
events={dynamicNewsEvents}
|
||||
loading={dynamicNewsLoading}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
onEventClick={handleEventClick}
|
||||
onViewDetail={handleViewDetail}
|
||||
/>
|
||||
|
||||
{/* 市场复盘 - 左右布局 */}
|
||||
<MarketReviewCard
|
||||
mt={6}
|
||||
events={marketReviewData.events}
|
||||
loading={loading}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
onEventClick={handleEventClick}
|
||||
onViewDetail={handleViewDetail}
|
||||
onToggleFollow={() => {}}
|
||||
/>
|
||||
|
||||
{/* 实时事件 - 原纵向列表 */}
|
||||
<EventTimelineCard
|
||||
ref={eventTimelineRef}
|
||||
mt={6}
|
||||
|
||||
Reference in New Issue
Block a user