refactor: Community 目录结构重组 + 修复导入路径 + 添加 Mock 数据
## 目录重构 - DynamicNewsCard/ → DynamicNews/(含 layouts/, hooks/ 子目录) - EventCard 原子组件 → EventCard/atoms/ - EventDetailModal 独立目录化 - HotEvents 独立目录化(含 CSS) - SearchFilters 独立目录化(CompactSearchBox, TradingTimeFilter) ## 导入路径修复 - EventCard/*.js: 统一使用 @constants/, @utils/, @components/ 别名 - atoms/*.js: 修复移动后的相对路径问题 - DynamicNewsCard.js: 更新 contexts, store, constants 导入 - EventHeaderInfo.js, CompactMetaBar.js: 修复 EventFollowButton 导入 ## Mock Handler 添加 - /api/events/:eventId/expectation-score - 事件超预期得分 - /api/index/:indexCode/realtime - 指数实时行情 ## 警告修复 - CitationMark.js: overlayInnerStyle → styles (Antd 5.x) - CitedContent.js: 移除不支持的 jsx 属性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// src/views/Community/components/EventCard/EventDescription.js
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, Button } from '@chakra-ui/react';
|
||||
|
||||
/**
|
||||
* 事件描述组件(支持展开/收起)
|
||||
* @param {Object} props
|
||||
* @param {string} props.description - 描述文本
|
||||
* @param {string} props.textColor - 文字颜色
|
||||
* @param {number} props.minLength - 触发展开/收起的最小长度(默认 120)
|
||||
* @param {number} props.noOfLines - 未展开时显示的行数(默认 3)
|
||||
*/
|
||||
const EventDescription = ({
|
||||
description,
|
||||
textColor,
|
||||
minLength = 120,
|
||||
noOfLines = 3
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// 如果没有描述,不渲染
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text
|
||||
color={textColor}
|
||||
fontSize="sm"
|
||||
lineHeight="tall"
|
||||
noOfLines={isExpanded ? undefined : noOfLines}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
{description.length > minLength && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
colorScheme="blue"
|
||||
onClick={handleToggle}
|
||||
mt={1}
|
||||
>
|
||||
{isExpanded ? '收起' : '...展开'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDescription;
|
||||
@@ -0,0 +1,64 @@
|
||||
// src/views/Community/components/EventCard/atoms/EventFollowButton.js
|
||||
import React from 'react';
|
||||
import { IconButton, Box } from '@chakra-ui/react';
|
||||
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||
|
||||
/**
|
||||
* 事件关注按钮组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isFollowing - 是否已关注
|
||||
* @param {number} props.followerCount - 关注数
|
||||
* @param {Function} props.onToggle - 切换关注状态的回调函数
|
||||
* @param {string} props.size - 按钮尺寸('xs' | 'sm' | 'md',默认 'sm')
|
||||
* @param {boolean} props.showCount - 是否显示关注数(默认 true)
|
||||
*/
|
||||
const EventFollowButton = ({
|
||||
isFollowing,
|
||||
followerCount = 0,
|
||||
onToggle,
|
||||
size = 'sm',
|
||||
showCount = true
|
||||
}) => {
|
||||
const iconSize = size === 'xs' ? '16px' : size === 'sm' ? '18px' : '22px';
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
onToggle?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="inline-flex" alignItems="center" gap={1}>
|
||||
<IconButton
|
||||
size={size}
|
||||
colorScheme="yellow"
|
||||
variant="ghost"
|
||||
bg="rgba(113, 128, 150, 0.6)"
|
||||
boxShadow="sm"
|
||||
_hover={{
|
||||
bg: 'rgba(113, 128, 150, 0.8)',
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
icon={
|
||||
isFollowing ? (
|
||||
<AiFillStar
|
||||
size={iconSize}
|
||||
color="gold"
|
||||
/>
|
||||
) : (
|
||||
<AiOutlineStar
|
||||
size={iconSize}
|
||||
color="gold"
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handleClick}
|
||||
aria-label={isFollowing ? '取消关注' : '关注'}
|
||||
/>
|
||||
{/* <Box fontSize="xs" color="gray.500">
|
||||
{followerCount || 0}
|
||||
</Box> */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventFollowButton;
|
||||
100
src/views/Community/components/EventCard/atoms/EventHeader.js
Normal file
100
src/views/Community/components/EventCard/atoms/EventHeader.js
Normal 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;
|
||||
@@ -0,0 +1,96 @@
|
||||
// src/views/Community/components/EventCard/atoms/EventImportanceBadge.js
|
||||
import React from 'react';
|
||||
import { Badge, Tooltip, VStack, HStack, Text, Divider, Circle } from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import { getImportanceConfig, getAllImportanceLevels } from '@constants/importanceLevels';
|
||||
|
||||
/**
|
||||
* 事件重要性等级标签组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.importance - 重要性等级(S/A/B/C/D)
|
||||
* @param {boolean} props.showTooltip - 是否显示详细提示框(默认 false)
|
||||
* @param {boolean} props.showIcon - 是否显示信息图标(默认 false)
|
||||
* @param {string} props.size - 标签大小(xs/sm/md/lg,默认 xs)
|
||||
*/
|
||||
const EventImportanceBadge = ({
|
||||
importance,
|
||||
showTooltip = false,
|
||||
showIcon = false,
|
||||
size = 'xs'
|
||||
}) => {
|
||||
const importanceConfig = getImportanceConfig(importance);
|
||||
|
||||
// 简单模式:只显示标签
|
||||
if (!showTooltip) {
|
||||
return (
|
||||
<Badge
|
||||
colorScheme={importanceConfig.color.split('.')[0]}
|
||||
fontSize={size}
|
||||
px={size === 'xs' ? 2 : size === 'sm' ? 2.5 : 3}
|
||||
py={size === 'xs' ? 1 : size === 'sm' ? 1 : 1.5}
|
||||
borderRadius="md"
|
||||
fontWeight="bold"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
verticalAlign="middle"
|
||||
>
|
||||
{importance || 'C'}级
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// 详细模式:带提示框的标签
|
||||
return (
|
||||
<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={importanceConfig.color.split('.')[0]}
|
||||
px={1.5}
|
||||
py={0.5}
|
||||
borderRadius="md"
|
||||
fontSize={size}
|
||||
cursor="help"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
flexShrink={0}
|
||||
>
|
||||
{showIcon && <InfoIcon boxSize={2.5} />}
|
||||
{importance || 'C'}级
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventImportanceBadge;
|
||||
@@ -0,0 +1,155 @@
|
||||
// src/views/Community/components/EventCard/atoms/EventPriceDisplay.js
|
||||
import React, { useState } from 'react';
|
||||
import { HStack, Box, Text, Tooltip, Progress } from '@chakra-ui/react';
|
||||
import { PriceArrow } from '@utils/priceFormatters';
|
||||
|
||||
/**
|
||||
* 事件价格变动显示组件
|
||||
* @param {Object} props
|
||||
* @param {number|null} props.avgChange - 平均涨跌幅
|
||||
* @param {number|null} props.maxChange - 最大涨跌幅
|
||||
* @param {number|null} props.expectationScore - 超预期得分(满分100)
|
||||
* @param {boolean} props.compact - 是否为紧凑模式(只显示平均值,默认 false)
|
||||
* @param {boolean} props.inline - 是否内联显示(默认 false)
|
||||
*/
|
||||
const EventPriceDisplay = ({
|
||||
avgChange,
|
||||
maxChange,
|
||||
expectationScore,
|
||||
compact = false,
|
||||
inline = false
|
||||
}) => {
|
||||
// 点击切换显示最大超额/平均超额
|
||||
const [showAvg, setShowAvg] = useState(false);
|
||||
|
||||
// 获取颜色方案
|
||||
const getColorScheme = (value) => {
|
||||
if (value == null) return 'gray';
|
||||
return value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
|
||||
};
|
||||
|
||||
// 格式化百分比
|
||||
const formatPercent = (value) => {
|
||||
if (value == null) return '--';
|
||||
return `${value > 0 ? '+' : ''}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// 获取超预期得分的颜色(渐变色系)
|
||||
const getScoreColor = (score) => {
|
||||
if (score == null) return { bg: 'gray.100', color: 'gray.500', progressColor: 'gray' };
|
||||
if (score >= 80) return { bg: 'red.50', color: 'red.600', progressColor: 'red' };
|
||||
if (score >= 60) return { bg: 'orange.50', color: 'orange.600', progressColor: 'orange' };
|
||||
if (score >= 40) return { bg: 'yellow.50', color: 'yellow.700', progressColor: 'yellow' };
|
||||
if (score >= 20) return { bg: 'blue.50', color: 'blue.600', progressColor: 'blue' };
|
||||
return { bg: 'gray.50', color: 'gray.600', progressColor: 'gray' };
|
||||
};
|
||||
|
||||
// 紧凑模式:只显示平均值,内联在标题后
|
||||
if (compact && avgChange != null) {
|
||||
return (
|
||||
<Tooltip label="平均超额" placement="top">
|
||||
<Box
|
||||
bg={avgChange > 0 ? 'red.50' : avgChange < 0 ? 'green.50' : 'gray.100'}
|
||||
color={avgChange > 0 ? 'red.600' : avgChange < 0 ? 'green.600' : 'gray.500'}
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
fontWeight="bold"
|
||||
display={inline ? "inline-flex" : "flex"}
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
verticalAlign="middle"
|
||||
>
|
||||
<PriceArrow value={avgChange} />
|
||||
{formatPercent(avgChange)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const displayValue = showAvg ? avgChange : maxChange;
|
||||
const displayLabel = showAvg ? '平均超额' : '最大超额';
|
||||
const scoreColors = getScoreColor(expectationScore);
|
||||
|
||||
// 详细模式:显示最大超额(可点击切换)+ 超预期得分
|
||||
return (
|
||||
<HStack spacing={3} flexWrap="wrap">
|
||||
{/* 最大超额/平均超额 - 点击切换 */}
|
||||
<Tooltip
|
||||
label={showAvg ? "点击查看最大超额" : "点击查看平均超额"}
|
||||
placement="top"
|
||||
hasArrow
|
||||
>
|
||||
<Box
|
||||
bg={displayValue > 0 ? 'red.50' : displayValue < 0 ? 'green.50' : 'gray.100'}
|
||||
color={displayValue > 0 ? 'red.600' : displayValue < 0 ? 'green.600' : 'gray.500'}
|
||||
fontSize="xs"
|
||||
px={2.5}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
cursor="pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowAvg(!showAvg);
|
||||
}}
|
||||
_hover={{
|
||||
transform: 'scale(1.02)',
|
||||
boxShadow: 'sm',
|
||||
opacity: 0.9
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
border="1px solid"
|
||||
borderColor={displayValue > 0 ? 'red.200' : displayValue < 0 ? 'green.200' : 'gray.200'}
|
||||
>
|
||||
<HStack spacing={1.5}>
|
||||
<Text fontSize="xs" opacity={0.7} fontWeight="medium">{displayLabel}</Text>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{formatPercent(displayValue)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
{/* 超预期得分 - 精致的进度条样式 */}
|
||||
{expectationScore != null && (
|
||||
<Tooltip
|
||||
label={`超预期得分:${expectationScore.toFixed(0)}分(满分100分)`}
|
||||
placement="top"
|
||||
hasArrow
|
||||
>
|
||||
<Box
|
||||
bg={scoreColors.bg}
|
||||
px={2.5}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={`${scoreColors.progressColor}.200`}
|
||||
minW="90px"
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="xs" color={scoreColors.color} fontWeight="medium" opacity={0.8}>
|
||||
超预期
|
||||
</Text>
|
||||
<Box flex={1} minW="40px">
|
||||
<Progress
|
||||
value={expectationScore}
|
||||
max={100}
|
||||
size="xs"
|
||||
colorScheme={scoreColors.progressColor}
|
||||
borderRadius="full"
|
||||
bg={`${scoreColors.progressColor}.100`}
|
||||
/>
|
||||
</Box>
|
||||
<Text fontSize="xs" fontWeight="bold" color={scoreColors.color}>
|
||||
{expectationScore.toFixed(0)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventPriceDisplay;
|
||||
58
src/views/Community/components/EventCard/atoms/EventStats.js
Normal file
58
src/views/Community/components/EventCard/atoms/EventStats.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// src/views/Community/components/EventCard/EventStats.js
|
||||
import React from 'react';
|
||||
import { HStack, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { ViewIcon, ChatIcon, StarIcon } from '@chakra-ui/icons';
|
||||
|
||||
/**
|
||||
* 事件统计信息组件(浏览量、帖子数、关注数)
|
||||
* @param {Object} props
|
||||
* @param {number} props.viewCount - 浏览量
|
||||
* @param {number} props.postCount - 帖子数
|
||||
* @param {number} props.followerCount - 关注数
|
||||
* @param {string} props.size - 尺寸('sm' | 'md',默认 'sm')
|
||||
* @param {number} props.spacing - 间距(默认 3)
|
||||
* @param {Object} props.display - 响应式显示控制(默认 { base: 'none', md: 'flex' })
|
||||
* @param {string} props.mutedColor - 文字颜色(可选)
|
||||
*/
|
||||
const EventStats = ({
|
||||
viewCount = 0,
|
||||
postCount = 0,
|
||||
followerCount = 0,
|
||||
size = 'sm',
|
||||
spacing = 3,
|
||||
display = { base: 'none', md: 'flex' },
|
||||
mutedColor
|
||||
}) => {
|
||||
const fontSize = size === 'sm' ? 'xs' : 'sm';
|
||||
const iconSize = size === 'sm' ? '12px' : '16px';
|
||||
|
||||
return (
|
||||
<HStack spacing={spacing} display={display} color={mutedColor}>
|
||||
{/* 浏览量 */}
|
||||
<Tooltip label="浏览量" placement="top">
|
||||
<HStack spacing={1}>
|
||||
<ViewIcon boxSize={iconSize} />
|
||||
<Text fontSize={fontSize}>{viewCount}</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
|
||||
{/* 帖子数 */}
|
||||
<Tooltip label="帖子数" placement="top">
|
||||
<HStack spacing={1}>
|
||||
<ChatIcon boxSize={iconSize} />
|
||||
<Text fontSize={fontSize}>{postCount}</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
|
||||
{/* 关注数 */}
|
||||
<Tooltip label="关注数" placement="top">
|
||||
<HStack spacing={1}>
|
||||
<StarIcon boxSize={iconSize} />
|
||||
<Text fontSize={fontSize}>{followerCount}</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventStats;
|
||||
@@ -0,0 +1,84 @@
|
||||
// src/views/Community/components/EventCard/EventTimeline.js
|
||||
import React from 'react';
|
||||
import { Box, VStack, Text, useColorModeValue, Badge } from '@chakra-ui/react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 事件时间轴组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.createdAt - 事件创建时间
|
||||
* @param {Object} props.timelineStyle - 时间轴样式配置
|
||||
* @param {string} props.borderColor - 竖线边框颜色
|
||||
* @param {string} props.minHeight - 竖线最小高度(例如:'40px' 或 '80px')
|
||||
* @param {Object} props.importance - 重要性配置对象(包含 level, badgeBg, badgeColor 等)
|
||||
*/
|
||||
const EventTimeline = ({ createdAt, timelineStyle, borderColor, minHeight = '40px', importance }) => {
|
||||
return (
|
||||
<VStack spacing={0} align="center" minW="65px" position="relative">
|
||||
{/* 时间长方形卡片 - 更紧凑 */}
|
||||
<Box
|
||||
{...(timelineStyle.bgGradient ? { bgGradient: timelineStyle.bgGradient } : { bg: timelineStyle.bg })}
|
||||
borderWidth={timelineStyle.borderWidth}
|
||||
borderColor={timelineStyle.borderColor}
|
||||
borderRadius="md"
|
||||
px={1.5}
|
||||
py={1.5}
|
||||
minW="60px"
|
||||
textAlign="center"
|
||||
boxShadow={timelineStyle.boxShadow}
|
||||
transition="all 0.3s ease"
|
||||
position="relative"
|
||||
>
|
||||
{/* 重要性等级徽章 - 左上角 */}
|
||||
{importance && (
|
||||
<Badge
|
||||
position="absolute"
|
||||
top="-6px"
|
||||
left="-6px"
|
||||
bg={importance.badgeBg}
|
||||
color={importance.badgeColor}
|
||||
fontSize="10px"
|
||||
fontWeight="bold"
|
||||
borderRadius="full"
|
||||
px={1.5}
|
||||
py={0.5}
|
||||
boxShadow="md"
|
||||
zIndex={2}
|
||||
>
|
||||
{importance.level}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* 日期 MM-DD */}
|
||||
<Text
|
||||
fontSize="10px"
|
||||
fontWeight="bold"
|
||||
color={timelineStyle.textColor}
|
||||
lineHeight="1.2"
|
||||
>
|
||||
{dayjs(createdAt).format('MM-DD')}
|
||||
</Text>
|
||||
{/* 时间 HH:mm */}
|
||||
<Text
|
||||
fontSize="10px"
|
||||
fontWeight="bold"
|
||||
color={timelineStyle.textColor}
|
||||
lineHeight="1.2"
|
||||
mt={0.5}
|
||||
>
|
||||
{dayjs(createdAt).format('HH:mm')}
|
||||
</Text>
|
||||
</Box>
|
||||
{/* 时间轴竖线 */}
|
||||
<Box
|
||||
w="2px"
|
||||
flex="1"
|
||||
bg={borderColor}
|
||||
minH={minHeight}
|
||||
mt={1}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTimeline;
|
||||
@@ -0,0 +1,95 @@
|
||||
// src/views/Community/components/EventCard/atoms/ImportanceBadge.js
|
||||
// 重要性标签通用组件
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
HStack,
|
||||
VStack,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
PopoverArrow,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
import { getImportanceConfig, getAllImportanceLevels } from '@constants/importanceLevels';
|
||||
|
||||
/**
|
||||
* 重要性标签组件(实心样式)
|
||||
* @param {Object} props
|
||||
* @param {string} props.importance - 重要性等级 ('S' | 'A' | 'B' | 'C')
|
||||
* @param {Object} props.position - 定位配置 { top, left, right, bottom },默认为 { top: 0, left: 0 }
|
||||
*/
|
||||
const ImportanceBadge = ({ importance, position = { top: 0, left: 0 } }) => {
|
||||
const importanceConfig = getImportanceConfig(importance);
|
||||
|
||||
return (
|
||||
<Popover trigger="hover" placement="right" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={position.top}
|
||||
left={position.left}
|
||||
right={position.right}
|
||||
bottom={position.bottom}
|
||||
zIndex={1}
|
||||
bg={importanceConfig.badgeBg}
|
||||
color={importanceConfig.badgeColor || 'white'}
|
||||
fontSize="11px"
|
||||
fontWeight="bold"
|
||||
w="30px"
|
||||
h="15px"
|
||||
borderRadius="xl"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
cursor="help"
|
||||
boxShadow="sm"
|
||||
>
|
||||
{importanceConfig.label}
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent width="auto" maxW="350px">
|
||||
<PopoverArrow />
|
||||
<PopoverBody p={3}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<Text fontSize="sm" fontWeight="bold" mb={1}>
|
||||
重要性等级说明
|
||||
</Text>
|
||||
{getAllImportanceLevels().map(item => (
|
||||
<HStack key={item.level} spacing={2} align="center">
|
||||
<Box
|
||||
w="30px"
|
||||
h="15px"
|
||||
bg={item.badgeBg}
|
||||
color={item.badgeColor || 'white'}
|
||||
fontSize="9px"
|
||||
fontWeight="bold"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
borderRadius="lg"
|
||||
flexShrink={0}
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
<Text fontSize="xs" flex={1}>
|
||||
<Text as="span" fontWeight="bold">
|
||||
{item.label}:
|
||||
</Text>
|
||||
{item.description}
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportanceBadge;
|
||||
@@ -0,0 +1,86 @@
|
||||
// src/views/Community/components/EventCard/atoms/ImportanceStamp.js
|
||||
// 重要性印章组件
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { getImportanceConfig } from '@constants/importanceLevels';
|
||||
|
||||
/**
|
||||
* 重要性印章组件(模拟盖章效果)
|
||||
* @param {Object} props
|
||||
* @param {string} props.importance - 重要性等级 (S/A/B/C)
|
||||
* @param {string} props.size - 印章尺寸 ('sm' | 'md' | 'lg')
|
||||
*/
|
||||
const ImportanceStamp = ({ importance, size = 'md' }) => {
|
||||
const config = getImportanceConfig(importance);
|
||||
|
||||
// 印章颜色
|
||||
const stampColor = useColorModeValue(config.badgeBg, config.badgeBg);
|
||||
|
||||
// 尺寸配置
|
||||
const sizeConfig = {
|
||||
sm: { outer: '40px', inner: '34px', fontSize: 'md', borderOuter: '2px', borderInner: '1.5px' },
|
||||
md: { outer: '50px', inner: '42px', fontSize: 'xl', borderOuter: '3px', borderInner: '2px' },
|
||||
lg: { outer: '60px', inner: '52px', fontSize: '2xl', borderOuter: '4px', borderInner: '2.5px' },
|
||||
};
|
||||
|
||||
const currentSize = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
display="inline-block"
|
||||
>
|
||||
{/* 外层圆形边框(双圈) */}
|
||||
<Box
|
||||
position="relative"
|
||||
w={currentSize.outer}
|
||||
h={currentSize.outer}
|
||||
borderRadius="50%"
|
||||
borderWidth={currentSize.borderOuter}
|
||||
borderColor={stampColor}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
transform="rotate(-15deg)"
|
||||
opacity={0.9}
|
||||
boxShadow="0 3px 12px rgba(0,0,0,0.2)"
|
||||
bg={useColorModeValue('white', 'gray.800')}
|
||||
_hover={{
|
||||
opacity: 1,
|
||||
transform: "rotate(-15deg) scale(1.15)",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
transition="all 0.3s ease"
|
||||
>
|
||||
{/* 内层圆形边框 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
w={currentSize.inner}
|
||||
h={currentSize.inner}
|
||||
borderRadius="50%"
|
||||
borderWidth={currentSize.borderInner}
|
||||
borderColor={stampColor}
|
||||
/>
|
||||
|
||||
{/* 印章文字 */}
|
||||
<Text
|
||||
fontSize={currentSize.fontSize}
|
||||
fontWeight="black"
|
||||
color={stampColor}
|
||||
fontFamily={config.stampFont}
|
||||
letterSpacing="2px"
|
||||
textShadow="0 0 2px currentColor"
|
||||
>
|
||||
{config.stampText}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportanceStamp;
|
||||
@@ -0,0 +1,86 @@
|
||||
// src/views/Community/components/EventCard/atoms/KeywordsCarousel.js
|
||||
// Keywords标签组件(点击切换)
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
|
||||
/**
|
||||
* Keywords标签组件(点击切换下一个)
|
||||
* @param {Array} keywords - 关键词数组
|
||||
* @param {Function} onKeywordClick - 关键词点击回调
|
||||
*/
|
||||
const KeywordsCarousel = ({
|
||||
keywords = [],
|
||||
onKeywordClick
|
||||
}) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// 如果没有keywords,不渲染
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentKeyword = keywords[currentIndex];
|
||||
|
||||
// 点击切换到下一个关键词
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// 切换到下一个
|
||||
const nextIndex = (currentIndex + 1) % keywords.length;
|
||||
setCurrentIndex(nextIndex);
|
||||
|
||||
// 触发回调
|
||||
if (onKeywordClick) {
|
||||
onKeywordClick(keywords[nextIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={keywords.length > 1
|
||||
? `点击切换下一个标签 (${currentIndex + 1}/${keywords.length})`
|
||||
: '事件标签'}
|
||||
placement="top"
|
||||
hasArrow
|
||||
bg="rgba(20, 20, 20, 0.95)"
|
||||
color={PROFESSIONAL_COLORS.gold[500]}
|
||||
fontSize="sm"
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
right={4}
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
pointerEvents="auto"
|
||||
zIndex={1}
|
||||
cursor={keywords.length > 1 ? "pointer" : "default"}
|
||||
onClick={keywords.length > 1 ? handleClick : undefined}
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="full"
|
||||
bg="transparent"
|
||||
transition="all 0.3s ease"
|
||||
_hover={keywords.length > 1 ? {
|
||||
bg: 'rgba(255, 195, 0, 0.15)',
|
||||
transform: 'translateY(-50%) scale(1.05)',
|
||||
} : {}}
|
||||
>
|
||||
<Text
|
||||
color={PROFESSIONAL_COLORS.gold[500]}
|
||||
fontSize="md"
|
||||
fontWeight="bold"
|
||||
letterSpacing="wide"
|
||||
whiteSpace="nowrap"
|
||||
textShadow="0 0 20px rgba(255, 195, 0, 0.3)"
|
||||
>
|
||||
{typeof currentKeyword === 'string'
|
||||
? currentKeyword
|
||||
: currentKeyword?.concept || '未知标签'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeywordsCarousel;
|
||||
13
src/views/Community/components/EventCard/atoms/index.js
Normal file
13
src/views/Community/components/EventCard/atoms/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// src/views/Community/components/EventCard/atoms/index.js
|
||||
// 事件卡片原子组件
|
||||
|
||||
export { default as EventDescription } from './EventDescription';
|
||||
export { default as EventFollowButton } from './EventFollowButton';
|
||||
export { default as EventHeader } from './EventHeader';
|
||||
export { default as EventImportanceBadge } from './EventImportanceBadge';
|
||||
export { default as EventPriceDisplay } from './EventPriceDisplay';
|
||||
export { default as EventStats } from './EventStats';
|
||||
export { default as EventTimeline } from './EventTimeline';
|
||||
export { default as ImportanceBadge } from './ImportanceBadge';
|
||||
export { default as ImportanceStamp } from './ImportanceStamp';
|
||||
export { default as KeywordsCarousel } from './KeywordsCarousel';
|
||||
Reference in New Issue
Block a user