feat: 重构主组件

│ │
│ │ -  移除 renderPriceChange 函数(60行)                                                                                          │ │
│ │ -  移除 renderCompactEvent 函数(200行)                                                                                        │ │
│ │ -  移除 renderDetailedEvent 函数(300行)                                                                                       │ │
│ │ -  移除 expandedDescriptions state                                                                                              │ │
│ │ -  精简 Chakra UI 导入                                                                                                          │ │
│ │ -  使用 EventCard 组件统一渲染                                                                                                  │ │
│ │ -  保留所有业务逻辑(WebSocket、通知、关注)
This commit is contained in:
zdl
2025-10-30 12:15:55 +08:00
parent ff9f1fe2a1
commit 4a0194e26c

View File

@@ -7,68 +7,28 @@ import {
Text, Text,
Button, Button,
Badge, Badge,
Tag,
TagLabel,
TagLeftIcon,
Flex, Flex,
Avatar,
Tooltip,
IconButton,
Divider,
Container, Container,
useColorModeValue, useColorModeValue,
Circle,
Stat,
StatNumber,
StatHelpText,
StatArrow,
ButtonGroup,
Heading,
SimpleGrid,
Card,
CardBody,
Center,
Link,
Spacer,
Switch, Switch,
FormControl, FormControl,
FormLabel, FormLabel,
useToast, useToast,
Center,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import { InfoIcon } from '@chakra-ui/icons';
ViewIcon,
ChatIcon,
StarIcon,
TimeIcon,
InfoIcon,
WarningIcon,
WarningTwoIcon,
CheckCircleIcon,
ArrowForwardIcon,
ExternalLinkIcon,
ViewOffIcon,
} from '@chakra-ui/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import moment from 'moment';
// 导入工具函数和常量 // 导入工具函数和常量
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { getApiBase } from '../../../utils/apiConfig'; import { getApiBase } from '../../../utils/apiConfig';
import { useEventNotifications } from '../../../hooks/useEventNotifications'; import { useEventNotifications } from '../../../hooks/useEventNotifications';
import { getImportanceConfig, getAllImportanceLevels } from '../../../constants/importanceLevels';
import { browserNotificationService } from '../../../services/browserNotificationService'; import { browserNotificationService } from '../../../services/browserNotificationService';
import { useNotification } from '../../../contexts/NotificationContext'; import { useNotification } from '../../../contexts/NotificationContext';
import { getImportanceConfig } from '../../../constants/importanceLevels';
// 导入价格相关工具函数 // 导入子组件
import { import EventCard from './EventCard';
getPriceChangeColor,
getPriceChangeBg,
getPriceChangeBorderColor,
PriceArrow,
} from '../../../utils/priceFormatters';
// 导入动画定义
import { pulseAnimation } from '../../../constants/animations';
// ========== 主组件 ========== // ========== 主组件 ==========
const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => { const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => {
@@ -78,7 +38,6 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
const [followingMap, setFollowingMap] = useState({}); const [followingMap, setFollowingMap] = useState({});
const [followCountMap, setFollowCountMap] = useState({}); const [followCountMap, setFollowCountMap] = useState({});
const [localEvents, setLocalEvents] = useState(events); // 用于实时更新的本地事件列表 const [localEvents, setLocalEvents] = useState(events); // 用于实时更新的本地事件列表
const [expandedDescriptions, setExpandedDescriptions] = useState({}); // 描述展开状态映射
// 从 NotificationContext 获取推送权限相关状态和方法 // 从 NotificationContext 获取推送权限相关状态和方法
const { browserPermission, requestBrowserPermission } = useNotification(); const { browserPermission, requestBrowserPermission } = useNotification();
@@ -265,67 +224,6 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
const linkColor = useColorModeValue('blue.600', 'blue.400'); const linkColor = useColorModeValue('blue.600', 'blue.400');
const hoverBg = useColorModeValue('gray.50', 'gray.700'); const hoverBg = useColorModeValue('gray.50', 'gray.700');
const renderPriceChange = (value, label) => {
if (value === null || value === undefined) {
return (
<Tag size="lg" colorScheme="gray" borderRadius="full" variant="subtle">
<TagLabel fontSize="sm" fontWeight="medium">{label}: --</TagLabel>
</Tag>
);
}
const absValue = Math.abs(value);
const isPositive = value > 0;
// 根据涨跌幅大小选择不同的颜色深浅
let colorScheme = 'gray';
let variant = 'solid';
if (isPositive) {
// 上涨用红色系
if (absValue >= 3) {
colorScheme = 'red';
variant = 'solid'; // 深色
} else if (absValue >= 1) {
colorScheme = 'red';
variant = 'subtle'; // 中等
} else {
colorScheme = 'red';
variant = 'outline'; // 浅色
}
} else {
// 下跌用绿色系
if (absValue >= 3) {
colorScheme = 'green';
variant = 'solid'; // 深色
} else if (absValue >= 1) {
colorScheme = 'green';
variant = 'subtle'; // 中等
} else {
colorScheme = 'green';
variant = 'outline'; // 浅色
}
}
const Icon = isPositive ? TriangleUpIcon : TriangleDownIcon;
return (
<Tag
size="lg"
colorScheme={colorScheme}
borderRadius="full"
variant={variant}
boxShadow="sm"
_hover={{ transform: 'scale(1.05)', boxShadow: 'md' }}
transition="all 0.2s"
>
<TagLeftIcon as={Icon} boxSize="16px" />
<TagLabel fontSize="sm" fontWeight="bold">
{label}: {isPositive ? '+' : ''}{value.toFixed(2)}%
</TagLabel>
</Tag>
);
};
const handleTitleClick = (e, event) => { const handleTitleClick = (e, event) => {
e.preventDefault(); e.preventDefault();
@@ -349,511 +247,6 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
}; };
}; };
// 精简模式的事件渲染优化版标题2行+标签内联+按钮右侧)
const renderCompactEvent = (event, index) => {
const importance = getImportanceConfig(event.importance);
const isFollowing = !!followingMap[event.id];
const followerCount = followCountMap[event.id] ?? (event.follower_count || 0);
const timelineStyle = getTimelineBoxStyle();
return (
<HStack align="stretch" spacing={3} w="full">
{/* 左侧时间轴 - 动态样式 */}
<VStack spacing={0} align="center" minW="90px">
{/* 时间长方形卡片 */}
<Box
{...(timelineStyle.bgGradient ? { bgGradient: timelineStyle.bgGradient } : { bg: timelineStyle.bg })}
borderWidth={timelineStyle.borderWidth}
borderColor={timelineStyle.borderColor}
borderRadius="md"
px={2}
py={2}
minW="85px"
textAlign="center"
boxShadow={timelineStyle.boxShadow}
transition="all 0.3s ease"
>
{/* 日期 YYYY-MM-DD */}
<Text
fontSize="xs"
fontWeight="bold"
color={timelineStyle.textColor}
lineHeight="1.3"
>
{moment(event.created_at).format('YYYY-MM-DD')}
</Text>
{/* 时间 HH:mm */}
<Text
fontSize="xs"
fontWeight="bold"
color={timelineStyle.textColor}
lineHeight="1.3"
mt={0.5}
>
{moment(event.created_at).format('HH:mm')}
</Text>
</Box>
{/* 时间轴竖线 */}
<Box
w="2px"
flex="1"
bg={borderColor}
minH="40px"
mt={1}
/>
</VStack>
{/* 右侧内容卡片 */}
<Card
flex="1"
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)}
mb={2}
>
<CardBody p={3}>
<VStack align="stretch" spacing={2}>
{/* 第一行标题2行+ 标签(内联)+ 按钮(右侧) */}
<Flex align="flex-start" gap={2}>
{/* 标题区域:标题+标签(内联) */}
<Box flex="1" minW="150px">
<Text
fontSize="md"
fontWeight="bold"
color={linkColor}
lineHeight="1.4"
noOfLines={2}
display="inline"
_hover={{ textDecoration: 'underline', color: 'blue.500' }}
onClick={(e) => handleTitleClick(e, event)}
cursor="pointer"
>
{event.title}
</Text>
{' '}
{/* 重要性标签 - 内联 */}
<Badge
colorScheme={importance.color.split('.')[0]}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
fontWeight="bold"
display="inline-flex"
alignItems="center"
verticalAlign="middle"
>
{event.importance || 'C'}
</Badge>
{' '}
{/* 涨跌幅标签 - 内联 */}
{event.related_avg_chg != null && (
<Tooltip label="平均" placement="top">
<Badge
colorScheme={event.related_avg_chg > 0 ? 'red' : 'green'}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
fontWeight="bold"
display="inline-flex"
alignItems="center"
gap={1}
verticalAlign="middle"
>
<PriceArrow value={event.related_avg_chg} />
{event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}%
</Badge>
</Tooltip>
)}
</Box>
{/* 操作按钮 - 固定右侧 */}
<HStack spacing={2} flexShrink={0}>
<Button
size="xs"
variant="ghost"
colorScheme="blue"
onClick={(e) => handleViewDetailClick(e, event.id)}
>
详情
</Button>
<Button
size="xs"
variant={isFollowing ? 'solid' : 'outline'}
colorScheme="yellow"
leftIcon={<StarIcon boxSize="10px" />}
onClick={(e) => {
e.stopPropagation();
toggleFollow(event.id);
}}
>
{isFollowing ? '已关注' : '关注'}
{followerCount > 0 && `(${followerCount})`}
</Button>
</HStack>
</Flex>
{/* 第二行:统计数据(左) + 作者时间(右) */}
<Flex justify="space-between" align="center" fontSize="xs" color={mutedColor}>
{/* 左侧:统计数据 */}
<HStack spacing={3} display={{ base: 'none', md: 'flex' }}>
<Tooltip label="浏览量" placement="top">
<HStack spacing={1}>
<ViewIcon boxSize="12px" />
<Text>{event.view_count || 0}</Text>
</HStack>
</Tooltip>
<Tooltip label="帖子数" placement="top">
<HStack spacing={1}>
<ChatIcon boxSize="12px" />
<Text>{event.post_count || 0}</Text>
</HStack>
</Tooltip>
<Tooltip label="关注数" placement="top">
<HStack spacing={1}>
<StarIcon boxSize="12px" />
<Text>{followerCount}</Text>
</HStack>
</Tooltip>
</HStack>
{/* 右侧:作者 + 时间(统一格式 YYYY-MM-DD HH:mm */}
<HStack spacing={2}>
<Text>@{event.creator?.username || 'Anonymous'}</Text>
<Text></Text>
<Text fontWeight="bold" color={linkColor}>
{moment(event.created_at).format('YYYY-MM-DD HH:mm')}
</Text>
</HStack>
</Flex>
</VStack>
</CardBody>
</Card>
</HStack>
);
};
// 详细模式的事件渲染(原有的渲染方式,但修复了箭头颜色)
const renderDetailedEvent = (event) => {
const importance = getImportanceConfig(event.importance);
const isFollowing = !!followingMap[event.id];
const followerCount = followCountMap[event.id] ?? (event.follower_count || 0);
const timelineStyle = getTimelineBoxStyle();
return (
<HStack align="stretch" spacing={3} w="full">
{/* 左侧时间轴 - 动态样式 */}
<VStack spacing={0} align="center" minW="90px">
{/* 时间长方形卡片 */}
<Box
{...(timelineStyle.bgGradient ? { bgGradient: timelineStyle.bgGradient } : { bg: timelineStyle.bg })}
borderWidth={timelineStyle.borderWidth}
borderColor={timelineStyle.borderColor}
borderRadius="md"
px={2}
py={2}
minW="85px"
textAlign="center"
boxShadow={timelineStyle.boxShadow}
transition="all 0.3s ease"
>
{/* 日期 YYYY-MM-DD */}
<Text
fontSize="xs"
fontWeight="bold"
color={timelineStyle.textColor}
lineHeight="1.3"
>
{moment(event.created_at).format('YYYY-MM-DD')}
</Text>
{/* 时间 HH:mm */}
<Text
fontSize="xs"
fontWeight="bold"
color={timelineStyle.textColor}
lineHeight="1.3"
mt={0.5}
>
{moment(event.created_at).format('HH:mm')}
</Text>
</Box>
{/* 时间轴竖线 */}
<Box
w="2px"
flex="1"
bg={borderColor}
minH="80px"
mt={1}
/>
</VStack>
{/* 事件卡片 */}
<Card
flex="1"
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
boxShadow="sm"
_hover={{
boxShadow: 'xl',
transform: 'translateY(-3px)',
borderColor: importance.color,
}}
transition="all 0.3s ease"
cursor="pointer"
onClick={() => onEventClick(event)}
mb={3}
>
<CardBody p={4}>
<VStack align="stretch" spacing={2.5}>
{/* 第一行:标题+优先级 | 统计+关注 */}
<Flex align="center" justify="space-between" gap={3}>
{/* 左侧:标题 + 优先级标签 */}
<HStack spacing={2} flex="1" align="center">
<Tooltip
label="点击查看事件详情"
placement="top"
hasArrow
openDelay={500}
>
<Heading
size="md"
color={linkColor}
_hover={{ textDecoration: 'underline', color: 'blue.500' }}
onClick={(e) => handleTitleClick(e, event)}
cursor="pointer"
>
{event.title}
</Heading>
</Tooltip>
<Tooltip
label={
<VStack align="start" spacing={1} maxW="320px">
<Text fontWeight="bold" fontSize="sm" mb={1}>
重要性等级说明
</Text>
<Divider borderColor="gray.300" />
{getAllImportanceLevels().map((level) => (
<HStack key={level.level} spacing={2} align="center" w="full" py={0.5}>
<Circle
size="8px"
bg={level.dotBg}
flexShrink={0}
/>
<Text fontSize="xs" color="gray.700" lineHeight="1.5">
<Text as="span" fontWeight="bold">{level.level}</Text>
{level.description}
</Text>
</HStack>
))}
</VStack>
}
placement="top"
hasArrow
bg="white"
color="gray.800"
fontSize="md"
p={3}
borderRadius="lg"
borderWidth="1px"
borderColor="gray.200"
boxShadow="lg"
>
<Badge
colorScheme={importance.color.split('.')[0]}
px={1.5}
py={0.5}
borderRadius="md"
fontSize="xs"
cursor="help"
display="flex"
alignItems="center"
gap={1}
flexShrink={0}
>
<InfoIcon boxSize={2.5} />
{event.importance || 'C'}
</Badge>
</Tooltip>
</HStack>
{/* 右侧:统计数据 + 关注按钮 */}
<HStack spacing={4} flexShrink={0}>
{/* 统计数据 */}
<HStack spacing={4}>
<Tooltip label="浏览量" placement="top">
<HStack spacing={1} color={mutedColor}>
<ViewIcon />
<Text fontSize="sm">{event.view_count || 0}</Text>
</HStack>
</Tooltip>
<Tooltip label="帖子数" placement="top">
<HStack spacing={1} color={mutedColor}>
<ChatIcon />
<Text fontSize="sm">{event.post_count || 0}</Text>
</HStack>
</Tooltip>
<Tooltip label="关注数" placement="top">
<HStack spacing={1} color={mutedColor}>
<StarIcon />
<Text fontSize="sm">{followerCount}</Text>
</HStack>
</Tooltip>
</HStack>
{/* 关注按钮 */}
<Button
size="sm"
colorScheme="yellow"
variant={isFollowing ? 'solid' : 'outline'}
leftIcon={<StarIcon boxSize="12px" />}
onClick={(e) => {
e.stopPropagation();
toggleFollow(event.id);
}}
>
{isFollowing ? '已关注' : '关注'}
</Button>
</HStack>
</Flex>
{/* 第二行:价格标签 | 时间+作者 */}
<Flex justify="space-between" align="center" wrap="wrap" gap={3}>
{/* 左侧:价格标签 */}
<HStack spacing={2} flexWrap="wrap">
{/* 平均涨幅 - 始终显示,无数据时显示 -- */}
<Badge
colorScheme={event.related_avg_chg != null
? (event.related_avg_chg > 0 ? 'red' : event.related_avg_chg < 0 ? 'green' : 'gray')
: 'gray'}
fontSize="xs"
px={2}
py={0.5}
borderRadius="md"
cursor="pointer"
_hover={{ transform: 'scale(1.05)', boxShadow: 'md' }}
transition="all 0.2s"
>
<HStack spacing={1}>
<Text fontSize="xs" opacity={0.8}>平均</Text>
<Text fontWeight="bold">
{event.related_avg_chg != null
? `${event.related_avg_chg > 0 ? '+' : ''}${event.related_avg_chg.toFixed(2)}%`
: '--'}
</Text>
</HStack>
</Badge>
{/* 最大涨幅 - 始终显示,无数据时显示 -- */}
<Badge
colorScheme={event.related_max_chg != null
? (event.related_max_chg > 0 ? 'red' : event.related_max_chg < 0 ? 'green' : 'gray')
: 'gray'}
fontSize="xs"
px={2}
py={0.5}
borderRadius="md"
cursor="pointer"
_hover={{ transform: 'scale(1.05)', boxShadow: 'md' }}
transition="all 0.2s"
>
<HStack spacing={1}>
<Text fontSize="xs" opacity={0.8}>最大</Text>
<Text fontWeight="bold">
{event.related_max_chg != null
? `${event.related_max_chg > 0 ? '+' : ''}${event.related_max_chg.toFixed(2)}%`
: '--'}
</Text>
</HStack>
</Badge>
{/* 周涨幅 - 始终显示,无数据时显示 -- */}
<Badge
colorScheme={event.related_week_chg != null
? (event.related_week_chg > 0 ? 'red' : event.related_week_chg < 0 ? 'green' : 'gray')
: 'gray'}
fontSize="xs"
px={2}
py={0.5}
borderRadius="md"
cursor="pointer"
_hover={{ transform: 'scale(1.05)', boxShadow: 'md' }}
transition="all 0.2s"
>
<HStack spacing={1}>
<Text fontSize="xs" opacity={0.8}></Text>
{event.related_week_chg != null && <PriceArrow value={event.related_week_chg} />}
<Text fontWeight="bold">
{event.related_week_chg != null
? `${event.related_week_chg > 0 ? '+' : ''}${event.related_week_chg.toFixed(2)}%`
: '--'}
</Text>
</HStack>
</Badge>
</HStack>
{/* 右侧:时间 + 作者 */}
<HStack spacing={2} fontSize="sm" flexShrink={0}>
<Text fontWeight="bold" color={linkColor}>
{moment(event.created_at).format('YYYY-MM-DD HH:mm')}
</Text>
<Text color={mutedColor}></Text>
<Text color={mutedColor}>@{event.creator?.username || 'Anonymous'}</Text>
</HStack>
</Flex>
{/* 第三行:描述文字 + 展开/收起 */}
{event.description && (
<Box>
<Text
color={textColor}
fontSize="sm"
lineHeight="tall"
noOfLines={expandedDescriptions[event.id] ? undefined : 3}
>
{event.description}
</Text>
{event.description.length > 120 && (
<Button
variant="link"
size="xs"
colorScheme="blue"
onClick={(e) => {
e.stopPropagation();
setExpandedDescriptions(prev => ({
...prev,
[event.id]: !prev[event.id]
}));
}}
mt={1}
>
{expandedDescriptions[event.id] ? '收起' : '...展开'}
</Button>
)}
</Box>
)}
</VStack>
</CardBody>
</Card>
</HStack>
);
};
// 分页组件 // 分页组件
const Pagination = ({ current, total, pageSize, onChange }) => { const Pagination = ({ current, total, pageSize, onChange }) => {
const totalPages = Math.ceil(total / pageSize); const totalPages = Math.ceil(total / pageSize);
@@ -1062,10 +455,19 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
<VStack align="stretch" spacing={0}> <VStack align="stretch" spacing={0}>
{localEvents.map((event, index) => ( {localEvents.map((event, index) => (
<Box key={event.id} position="relative"> <Box key={event.id} position="relative">
{isCompactMode <EventCard
? renderCompactEvent(event, index) event={event}
: renderDetailedEvent(event) index={index}
} isCompactMode={isCompactMode}
isFollowing={!!followingMap[event.id]}
followerCount={followCountMap[event.id] ?? (event.follower_count || 0)}
onEventClick={onEventClick}
onTitleClick={handleTitleClick}
onViewDetail={handleViewDetailClick}
onToggleFollow={toggleFollow}
timelineStyle={getTimelineBoxStyle()}
borderColor={borderColor}
/>
</Box> </Box>
))} ))}
</VStack> </VStack>