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;
|
||||
Reference in New Issue
Block a user