feat: 创建组合组件(Molecules)

- EventHeader: 标题头部组件(100行)                                                                                             │ │
│ │ - CompactEventCard: 紧凑模式卡片(160行)                                                                                        │ │
│ │ - DetailedEventCard: 详细模式卡片(170行)                                                                                       │ │
│ │ - index.js: EventCard 统一入口(60行)
This commit is contained in:
zdl
2025-10-30 12:15:03 +08:00
parent a39d57f9de
commit ff9f1fe2a1
4 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
// src/views/Community/components/EventCard/CompactEventCard.js
import React from 'react';
import {
HStack,
Card,
CardBody,
VStack,
Flex,
Box,
Button,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import moment from 'moment';
import { getImportanceConfig } from '../../../../constants/importanceLevels';
// 导入子组件
import EventTimeline from './EventTimeline';
import EventHeader from './EventHeader';
import EventStats from './EventStats';
import EventFollowButton from './EventFollowButton';
/**
* 紧凑模式事件卡片组件
* @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.onViewDetail - 查看详情事件
* @param {Function} props.onToggleFollow - 切换关注事件
* @param {Object} props.timelineStyle - 时间轴样式配置
* @param {string} props.borderColor - 边框颜色
*/
const CompactEventCard = ({
event,
index,
isFollowing,
followerCount,
onEventClick,
onTitleClick,
onViewDetail,
onToggleFollow,
timelineStyle,
borderColor,
}) => {
const importance = getImportanceConfig(event.importance);
const cardBg = useColorModeValue('white', 'gray.800');
const linkColor = useColorModeValue('blue.600', 'blue.400');
const mutedColor = useColorModeValue('gray.500', 'gray.400');
const handleViewDetailClick = (e) => {
e.stopPropagation();
onViewDetail?.(event.id);
};
return (
<HStack align="stretch" spacing={3} w="full">
{/* 左侧时间轴 */}
<EventTimeline
createdAt={event.created_at}
timelineStyle={timelineStyle}
borderColor={borderColor}
minHeight="40px"
/>
{/* 右侧内容卡片 */}
<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}>
{/* 标题区域:标题+标签(内联) */}
<EventHeader
title={event.title}
importance={event.importance}
onTitleClick={(e) => onTitleClick?.(e, event)}
linkColor={linkColor}
compact={true}
avgChange={event.related_avg_chg}
size="md"
/>
{/* 操作按钮 - 固定右侧 */}
<HStack spacing={2} flexShrink={0}>
<Button
size="xs"
variant="ghost"
colorScheme="blue"
onClick={handleViewDetailClick}
>
详情
</Button>
<EventFollowButton
isFollowing={isFollowing}
followerCount={followerCount}
onToggle={() => onToggleFollow?.(event.id)}
size="xs"
showCount={true}
/>
</HStack>
</Flex>
{/* 第二行:统计数据(左) + 作者时间(右) */}
<Flex justify="space-between" align="center" fontSize="xs" color={mutedColor}>
{/* 左侧:统计数据 */}
<EventStats
viewCount={event.view_count}
postCount={event.post_count}
followerCount={followerCount}
size="sm"
spacing={3}
display={{ base: 'none', md: 'flex' }}
mutedColor={mutedColor}
/>
{/* 右侧:作者 + 时间 */}
<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>
);
};
export default CompactEventCard;

View File

@@ -0,0 +1,151 @@
// src/views/Community/components/EventCard/DetailedEventCard.js
import React from 'react';
import {
HStack,
Card,
CardBody,
VStack,
Flex,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import moment from 'moment';
import { getImportanceConfig } from '../../../../constants/importanceLevels';
// 导入子组件
import EventTimeline from './EventTimeline';
import EventHeader from './EventHeader';
import EventStats from './EventStats';
import EventFollowButton from './EventFollowButton';
import EventPriceDisplay from './EventPriceDisplay';
import EventDescription from './EventDescription';
/**
* 详细模式事件卡片组件
* @param {Object} props
* @param {Object} props.event - 事件对象
* @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 DetailedEventCard = ({
event,
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');
const mutedColor = useColorModeValue('gray.500', 'gray.400');
const textColor = useColorModeValue('gray.700', 'gray.200');
return (
<HStack align="stretch" spacing={3} w="full">
{/* 左侧时间轴 */}
<EventTimeline
createdAt={event.created_at}
timelineStyle={timelineStyle}
borderColor={borderColor}
minHeight="80px"
/>
{/* 事件卡片 */}
<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}>
{/* 左侧:标题 + 优先级标签 */}
<EventHeader
title={event.title}
importance={event.importance}
onTitleClick={(e) => onTitleClick?.(e, event)}
linkColor={linkColor}
compact={false}
size="md"
/>
{/* 右侧:统计数据 + 关注按钮 */}
<HStack spacing={4} flexShrink={0}>
{/* 统计数据 */}
<EventStats
viewCount={event.view_count}
postCount={event.post_count}
followerCount={followerCount}
size="md"
spacing={4}
display="flex"
mutedColor={mutedColor}
/>
{/* 关注按钮 */}
<EventFollowButton
isFollowing={isFollowing}
followerCount={followerCount}
onToggle={() => onToggleFollow?.(event.id)}
size="sm"
showCount={false}
/>
</HStack>
</Flex>
{/* 第二行:价格标签 | 时间+作者 */}
<Flex justify="space-between" align="center" wrap="wrap" gap={3}>
{/* 左侧:价格标签 */}
<EventPriceDisplay
avgChange={event.related_avg_chg}
maxChange={event.related_max_chg}
weekChange={event.related_week_chg}
compact={false}
/>
{/* 右侧:时间 + 作者 */}
<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>
{/* 第三行:描述文字 + 展开/收起 */}
<EventDescription
description={event.description}
textColor={textColor}
minLength={120}
noOfLines={3}
/>
</VStack>
</CardBody>
</Card>
</HStack>
);
};
export default DetailedEventCard;

View File

@@ -0,0 +1,100 @@
// src/views/Community/components/EventCard/EventHeader.js
import React from 'react';
import { Box, Text, Heading, Tooltip, HStack } from '@chakra-ui/react';
import EventImportanceBadge from './EventImportanceBadge';
import EventPriceDisplay from './EventPriceDisplay';
/**
* 事件标题头部组件
* @param {Object} props
* @param {string} props.title - 事件标题
* @param {string} props.importance - 重要性等级
* @param {Function} props.onTitleClick - 标题点击事件
* @param {string} props.linkColor - 链接颜色
* @param {boolean} props.compact - 是否紧凑模式(默认 false
* @param {number|null} props.avgChange - 平均涨跌幅(紧凑模式下使用)
* @param {string} props.size - 标题大小('sm' | 'md' | 'lg',默认 'md'
*/
const EventHeader = ({
title,
importance,
onTitleClick,
linkColor,
compact = false,
avgChange = null,
size = 'md'
}) => {
const handleClick = (e) => {
e.preventDefault();
e.stopPropagation();
onTitleClick?.(e);
};
// 紧凑模式:标题 + 标签内联
if (compact) {
return (
<Box flex="1" minW="150px">
<Text
fontSize={size}
fontWeight="bold"
color={linkColor}
lineHeight="1.4"
noOfLines={2}
display="inline"
_hover={{ textDecoration: 'underline', color: 'blue.500' }}
onClick={handleClick}
cursor="pointer"
>
{title}
</Text>
{' '}
{/* 重要性标签 - 内联 */}
<EventImportanceBadge
importance={importance}
showTooltip={false}
size="xs"
/>
{' '}
{/* 价格标签 - 内联 */}
{avgChange != null && (
<EventPriceDisplay
avgChange={avgChange}
compact={true}
inline={true}
/>
)}
</Box>
);
}
// 详细模式:标题 + 提示框的重要性标签
return (
<HStack spacing={2} flex="1" align="center">
<Tooltip
label="点击查看事件详情"
placement="top"
hasArrow
openDelay={500}
>
<Heading
size={size}
color={linkColor}
_hover={{ textDecoration: 'underline', color: 'blue.500' }}
onClick={handleClick}
cursor="pointer"
>
{title}
</Heading>
</Tooltip>
<EventImportanceBadge
importance={importance}
showTooltip={true}
showIcon={true}
size="xs"
/>
</HStack>
);
};
export default EventHeader;

View File

@@ -0,0 +1,67 @@
// src/views/Community/components/EventCard/index.js
import React from 'react';
import CompactEventCard from './CompactEventCard';
import DetailedEventCard from './DetailedEventCard';
/**
* 事件卡片统一入口组件
* 根据 isCompactMode 自动选择紧凑模式或详细模式
*
* @param {Object} props
* @param {Object} props.event - 事件对象
* @param {number} props.index - 事件索引(紧凑模式下用于交替背景色)
* @param {boolean} props.isCompactMode - 是否为紧凑模式
* @param {boolean} props.isFollowing - 是否已关注
* @param {number} props.followerCount - 关注数
* @param {Function} props.onEventClick - 卡片点击事件
* @param {Function} props.onTitleClick - 标题点击事件
* @param {Function} props.onViewDetail - 查看详情事件(仅紧凑模式)
* @param {Function} props.onToggleFollow - 切换关注事件
* @param {Object} props.timelineStyle - 时间轴样式配置
* @param {string} props.borderColor - 边框颜色
*/
const EventCard = ({
event,
index,
isCompactMode,
isFollowing,
followerCount,
onEventClick,
onTitleClick,
onViewDetail,
onToggleFollow,
timelineStyle,
borderColor,
}) => {
if (isCompactMode) {
return (
<CompactEventCard
event={event}
index={index}
isFollowing={isFollowing}
followerCount={followerCount}
onEventClick={onEventClick}
onTitleClick={onTitleClick}
onViewDetail={onViewDetail}
onToggleFollow={onToggleFollow}
timelineStyle={timelineStyle}
borderColor={borderColor}
/>
);
}
return (
<DetailedEventCard
event={event}
isFollowing={isFollowing}
followerCount={followerCount}
onEventClick={onEventClick}
onTitleClick={onTitleClick}
onToggleFollow={onToggleFollow}
timelineStyle={timelineStyle}
borderColor={borderColor}
/>
);
};
export default EventCard;