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:
zdl
2025-10-31 14:11:03 +08:00
parent 5d8ad5e442
commit c372832f1f
11 changed files with 1211 additions and 23 deletions

View 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;