新增功能: - 实时要闻·动态追踪横向滚动卡片(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>
301 lines
9.8 KiB
JavaScript
301 lines
9.8 KiB
JavaScript
// 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;
|