chore: 清理 Community 目录下未使用的文件

- 删除 PageNavigationButton.js(功能已整合到 PaginationControl)
- 删除 useInfiniteScroll.js(已被 @tanstack/react-virtual 替代)
- 删除 EventDiscussionModal.js(已废弃)
- 删除 PopularKeywords.js(功能已整合到其他组件)
- 移除 index.js 中未使用的 useColorModeValue 导入

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-08 19:43:02 +08:00
parent 641514bbfd
commit 76f13d6098
5 changed files with 0 additions and 988 deletions

View File

@@ -1,83 +0,0 @@
// src/views/Community/components/DynamicNewsCard/PageNavigationButton.js
// 翻页导航按钮组件
import React from 'react';
import { IconButton, useColorModeValue } from '@chakra-ui/react';
import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
/**
* 翻页导航按钮组件
* @param {Object} props
* @param {'prev'|'next'} props.direction - 按钮方向prev=上一页next=下一页)
* @param {number} props.currentPage - 当前页码
* @param {number} props.totalPages - 总页数
* @param {Function} props.onPageChange - 翻页回调
* @param {string} props.mode - 显示模式只在carousel/grid模式下显示
*/
const PageNavigationButton = ({
direction,
currentPage,
totalPages,
onPageChange,
mode
}) => {
// 主题适配
const arrowBtnBg = useColorModeValue('rgba(255, 255, 255, 0.9)', 'rgba(0, 0, 0, 0.6)');
const arrowBtnHoverBg = useColorModeValue('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0.8)');
// 根据方向计算配置
const isPrev = direction === 'prev';
const isNext = direction === 'next';
const Icon = isPrev ? ChevronLeftIcon : ChevronRightIcon;
const position = isPrev ? 'left' : 'right';
const label = isPrev ? '上一页' : '下一页';
const targetPage = isPrev ? currentPage - 1 : currentPage + 1;
const shouldShow = isPrev
? currentPage > 1
: currentPage < totalPages;
const isDisabled = isNext ? currentPage >= totalPages : false;
// 判断是否显示(只在单排/双排模式显示)
const shouldRender = shouldShow && (mode === 'carousel' || mode === 'grid');
if (!shouldRender) return null;
const handleClick = () => {
console.log(
`%c🔵 [翻页] 点击${label}: 当前页${currentPage} → 目标页${targetPage} (共${totalPages}页)`,
'color: #3B82F6; font-weight: bold;'
);
onPageChange(targetPage);
};
return (
<IconButton
icon={<Icon boxSize={6} color="blue.500" />}
position="absolute"
{...{ [position]: 0 }}
top="50%"
transform="translateY(-50%)"
zIndex={2}
onClick={handleClick}
variant="ghost"
size="md"
w="40px"
h="40px"
minW="40px"
borderRadius="full"
bg={arrowBtnBg}
boxShadow="0 2px 8px rgba(0, 0, 0, 0.15)"
_hover={{
bg: arrowBtnHoverBg,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
transform: 'translateY(-50%) scale(1.05)'
}}
isDisabled={isDisabled}
aria-label={label}
title={label}
/>
);
};
export default PageNavigationButton;

View File

@@ -1,88 +0,0 @@
// src/views/Community/components/DynamicNewsCard/hooks/useInfiniteScroll.js
// 无限滚动 Hook
import { useEffect, useRef, useCallback } from 'react';
/**
* 无限滚动 Hook
* 监听容器滚动事件,当滚动到底部附近时触发加载更多数据
*
* @param {Object} options - 配置选项
* @param {Function} options.onLoadMore - 加载更多回调函数(返回 Promise
* @param {boolean} options.hasMore - 是否还有更多数据
* @param {boolean} options.isLoading - 是否正在加载
* @param {number} options.threshold - 触发阈值距离底部多少像素时触发默认200px
* @returns {Object} { containerRef } - 容器引用
*/
export const useInfiniteScroll = ({
onLoadMore,
hasMore = true,
isLoading = false,
threshold = 200
}) => {
const containerRef = useRef(null);
const isLoadingRef = useRef(false);
// 滚动处理函数
const handleScroll = useCallback(() => {
const container = containerRef.current;
// 检查条件:容器存在、未加载中、还有更多数据
if (!container || isLoadingRef.current || !hasMore) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceToBottom = scrollHeight - scrollTop - clientHeight;
// 距离底部小于阈值时触发加载
if (distanceToBottom < threshold) {
console.log(
'%c⬇ [懒加载] 触发加载下一页',
'color: #8B5CF6; font-weight: bold;',
{
scrollTop,
scrollHeight,
clientHeight,
distanceToBottom,
threshold
}
);
isLoadingRef.current = true;
// 调用加载函数并更新状态
onLoadMore()
.then(() => {
console.log('%c✅ [懒加载] 加载完成', 'color: #10B981; font-weight: bold;');
})
.catch((error) => {
console.error('%c❌ [懒加载] 加载失败', 'color: #DC2626; font-weight: bold;', error);
})
.finally(() => {
isLoadingRef.current = false;
});
}
}, [onLoadMore, hasMore, threshold]);
// 绑定滚动事件
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// 添加滚动监听
container.addEventListener('scroll', handleScroll, { passive: true });
// 清理函数
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
// 更新 loading 状态的 ref
useEffect(() => {
isLoadingRef.current = isLoading;
}, [isLoading]);
return { containerRef };
};

View File

@@ -1,614 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
Box,
Text,
VStack,
HStack,
Avatar,
Textarea,
Button,
Divider,
useToast,
Badge,
Flex,
IconButton,
Menu,
MenuButton,
MenuList,
MenuItem,
useColorModeValue,
Spinner,
Center,
Collapse,
Input,
} from '@chakra-ui/react';
import {
ChatIcon,
TimeIcon,
DeleteIcon,
EditIcon,
ChevronDownIcon,
TriangleDownIcon,
TriangleUpIcon,
} from '@chakra-ui/icons';
import { FaHeart, FaRegHeart, FaComment } from 'react-icons/fa';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { eventService } from '../../../services/eventService';
import { logger } from '../../../utils/logger';
const EventDiscussionModal = ({ isOpen, onClose, eventId, eventTitle, discussionType = '事件讨论' }) => {
const [posts, setPosts] = useState([]);
const [newPostContent, setNewPostContent] = useState('');
const [newPostTitle, setNewPostTitle] = useState('');
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [expandedPosts, setExpandedPosts] = useState({});
const [postComments, setPostComments] = useState({});
const [replyContents, setReplyContents] = useState({});
const [loadingComments, setLoadingComments] = useState({});
const toast = useToast();
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
// 加载帖子列表
const loadPosts = async () => {
if (!eventId) return;
setLoading(true);
try {
const response = await fetch(`/api/events/${eventId}/posts?sort=latest&page=1&per_page=20`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
setPosts(result.data || []);
logger.debug('EventDiscussionModal', '帖子列表加载成功', {
eventId,
postsCount: result.data?.length || 0
});
} else {
logger.error('EventDiscussionModal', 'loadPosts', new Error('API返回错误'), {
eventId,
status: response.status,
message: result.message
});
toast({
title: '加载帖子失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'loadPosts', error, { eventId });
toast({
title: '加载帖子失败',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
};
// 加载帖子的评论
const loadPostComments = async (postId) => {
setLoadingComments(prev => ({ ...prev, [postId]: true }));
try {
const response = await fetch(`/api/posts/${postId}/comments?sort=latest`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
setPostComments(prev => ({ ...prev, [postId]: result.data || [] }));
logger.debug('EventDiscussionModal', '评论加载成功', {
postId,
commentsCount: result.data?.length || 0
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'loadPostComments', error, { postId });
} finally {
setLoadingComments(prev => ({ ...prev, [postId]: false }));
}
};
// 切换展开/收起评论
const togglePostComments = async (postId) => {
const isExpanded = expandedPosts[postId];
if (!isExpanded) {
// 展开时加载评论
await loadPostComments(postId);
}
setExpandedPosts(prev => ({ ...prev, [postId]: !isExpanded }));
};
// 提交新帖子
const handleSubmitPost = async () => {
if (!newPostContent.trim()) return;
setSubmitting(true);
try {
const response = await fetch(`/api/events/${eventId}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title: newPostTitle.trim(),
content: newPostContent.trim(),
content_type: 'text',
})
});
const result = await response.json();
if (response.ok && result.success) {
setNewPostContent('');
setNewPostTitle('');
loadPosts();
logger.info('EventDiscussionModal', '帖子发布成功', {
eventId,
postId: result.data?.id
});
toast({
title: '帖子发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
} else {
logger.error('EventDiscussionModal', 'handleSubmitPost', new Error('API返回错误'), {
eventId,
message: result.message
});
toast({
title: result.message || '帖子发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleSubmitPost', error, { eventId });
toast({
title: '帖子发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setSubmitting(false);
}
};
// 删除帖子
const handleDeletePost = async (postId) => {
if (!window.confirm('确定要删除这个帖子吗?')) return;
try {
const response = await fetch(`/api/posts/${postId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
loadPosts();
logger.info('EventDiscussionModal', '帖子删除成功', { postId });
toast({
title: '帖子已删除',
status: 'success',
duration: 2000,
isClosable: true,
});
} else {
logger.error('EventDiscussionModal', 'handleDeletePost', new Error('API返回错误'), {
postId,
message: result.message
});
toast({
title: result.message || '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleDeletePost', error, { postId });
toast({
title: '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 点赞帖子
const handleLikePost = async (postId) => {
try {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
// 更新帖子列表中的点赞状态
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, likes_count: result.likes_count, liked: result.liked }
: post
));
logger.debug('EventDiscussionModal', '点赞操作成功', {
postId,
liked: result.liked,
likesCount: result.likes_count
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleLikePost', error, { postId });
toast({
title: '操作失败',
status: 'error',
duration: 2000,
isClosable: true,
});
}
};
// 提交评论
const handleSubmitComment = async (postId) => {
const content = replyContents[postId];
if (!content?.trim()) return;
try {
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
content: content.trim(),
})
});
const result = await response.json();
if (response.ok && result.success) {
setReplyContents(prev => ({ ...prev, [postId]: '' }));
// 重新加载该帖子的评论
await loadPostComments(postId);
// 更新帖子的评论数
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, comments_count: (post.comments_count || 0) + 1 }
: post
));
logger.info('EventDiscussionModal', '评论发布成功', {
postId,
commentId: result.data?.id
});
toast({
title: '评论发布成功',
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleSubmitComment', error, { postId });
toast({
title: '评论发布失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 删除评论
const handleDeleteComment = async (commentId, postId) => {
if (!window.confirm('确定要删除这条评论吗?')) return;
try {
const response = await fetch(`/api/comments/${commentId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
const result = await response.json();
if (response.ok && result.success) {
// 重新加载该帖子的评论
await loadPostComments(postId);
// 更新帖子的评论数
setPosts(prev => prev.map(post =>
post.id === postId
? { ...post, comments_count: Math.max(0, (post.comments_count || 0) - 1) }
: post
));
logger.info('EventDiscussionModal', '评论删除成功', { commentId, postId });
toast({
title: '评论已删除',
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
logger.error('EventDiscussionModal', 'handleDeleteComment', error, { commentId, postId });
toast({
title: '删除失败',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
useEffect(() => {
if (isOpen) {
loadPosts();
}
}, [isOpen, eventId]);
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent maxH="80vh">
<ModalHeader>
<VStack align="start" spacing={1}>
<HStack>
<ChatIcon />
<Text>{discussionType}</Text>
</HStack>
{eventTitle && (
<Text fontSize="sm" color="gray.500" fontWeight="normal">
{eventTitle}
</Text>
)}
</VStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody overflowY="auto">
{/* 发布新帖子 */}
<Box mb={4}>
<Input
value={newPostTitle}
onChange={(e) => setNewPostTitle(e.target.value)}
placeholder="帖子标题(可选)"
size="sm"
mb={2}
/>
<Textarea
value={newPostContent}
onChange={(e) => setNewPostContent(e.target.value)}
placeholder="分享您的观点..."
size="sm"
resize="vertical"
minH="80px"
/>
<Flex justify="flex-end" mt={2}>
<Button
colorScheme="blue"
size="sm"
onClick={handleSubmitPost}
isLoading={submitting}
isDisabled={!newPostContent.trim()}
>
发布帖子
</Button>
</Flex>
</Box>
<Divider mb={4} />
{/* 帖子列表 */}
{loading ? (
<Center py={8}>
<Spinner size="lg" />
</Center>
) : posts.length > 0 ? (
<VStack spacing={4} align="stretch">
{posts.map((post) => (
<Box
key={post.id}
p={4}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
transition="background 0.2s"
>
{/* 帖子头部 */}
<Flex justify="space-between" align="start" mb={3}>
<HStack align="start" spacing={3}>
<Avatar
size="sm"
name={post.user?.username || '匿名用户'}
src={post.user?.avatar_url}
/>
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Text fontWeight="semibold" fontSize="sm">
{post.user?.username || '匿名用户'}
</Text>
</HStack>
<HStack fontSize="xs" color="gray.500">
<TimeIcon />
<Text>
{format(new Date(post.created_at), 'MM月dd日 HH:mm', {
locale: zhCN,
})}
</Text>
</HStack>
</VStack>
</HStack>
{/* 操作菜单 */}
<Menu>
<MenuButton
as={IconButton}
icon={<ChevronDownIcon />}
variant="ghost"
size="sm"
/>
<MenuList>
<MenuItem
icon={<DeleteIcon />}
color="red.500"
onClick={() => handleDeletePost(post.id)}
>
删除帖子
</MenuItem>
</MenuList>
</Menu>
</Flex>
{/* 帖子标题 */}
{post.title && (
<Text fontSize="md" fontWeight="bold" mb={2}>
{post.title}
</Text>
)}
{/* 帖子内容 */}
<Text fontSize="sm" whiteSpace="pre-wrap" mb={3}>
{post.content}
</Text>
{/* 帖子操作栏 */}
<HStack spacing={4} mb={3}>
<Button
size="sm"
variant="ghost"
leftIcon={post.liked ? <FaHeart /> : <FaRegHeart />}
color={post.liked ? 'red.500' : 'gray.600'}
onClick={() => handleLikePost(post.id)}
>
{post.likes_count || 0}
</Button>
<Button
size="sm"
variant="ghost"
leftIcon={<FaComment />}
onClick={() => togglePostComments(post.id)}
rightIcon={expandedPosts[post.id] ? <TriangleUpIcon /> : <TriangleDownIcon />}
>
{post.comments_count || 0} 评论
</Button>
</HStack>
{/* 评论区 */}
<Collapse in={expandedPosts[post.id]} animateOpacity>
<Box borderTopWidth="1px" borderColor={borderColor} pt={3}>
{/* 评论输入框 */}
<HStack mb={3}>
<Textarea
size="sm"
placeholder="写下你的评论..."
value={replyContents[post.id] || ''}
onChange={(e) => setReplyContents(prev => ({ ...prev, [post.id]: e.target.value }))}
rows={2}
flex={1}
/>
<Button
size="sm"
colorScheme="blue"
onClick={() => handleSubmitComment(post.id)}
isDisabled={!replyContents[post.id]?.trim()}
>
评论
</Button>
</HStack>
{/* 评论列表 */}
{loadingComments[post.id] ? (
<Center py={4}>
<Spinner size="sm" />
</Center>
) : (
<VStack align="stretch" spacing={2}>
{postComments[post.id]?.map((comment) => (
<Box key={comment.id} pl={4} borderLeftWidth="2px" borderColor="gray.200">
<HStack justify="space-between" mb={1}>
<HStack spacing={2}>
<Avatar size="xs" name={comment.user?.username} src={comment.user?.avatar_url} />
<Text fontSize="sm" fontWeight="medium">
{comment.user?.username || '匿名用户'}
</Text>
<Text fontSize="xs" color="gray.500">
{format(new Date(comment.created_at), 'MM-dd HH:mm')}
</Text>
</HStack>
<IconButton
size="xs"
icon={<DeleteIcon />}
variant="ghost"
onClick={() => handleDeleteComment(comment.id, post.id)}
/>
</HStack>
<Text fontSize="sm" pl={7}>
{comment.content}
</Text>
{/* 显示回复 */}
{comment.replies && comment.replies.length > 0 && (
<VStack align="stretch" mt={2} spacing={1} pl={4}>
{comment.replies.map((reply) => (
<Box key={reply.id}>
<HStack spacing={1}>
<Text fontSize="xs" fontWeight="medium">
{reply.user?.username}:
</Text>
<Text fontSize="xs">{reply.content}</Text>
</HStack>
</Box>
))}
</VStack>
)}
</Box>
))}
{(!postComments[post.id] || postComments[post.id].length === 0) && (
<Text fontSize="sm" color="gray.500" textAlign="center" py={2}>
暂无评论
</Text>
)}
</VStack>
)}
</Box>
</Collapse>
</Box>
))}
</VStack>
) : (
<Center py={8}>
<VStack>
<ChatIcon boxSize={8} color="gray.400" />
<Text color="gray.500">暂无帖子快来发表您的观点吧</Text>
</VStack>
</Center>
)}
</ModalBody>
</ModalContent>
</Modal>
);
};
export default EventDiscussionModal;

View File

@@ -1,202 +0,0 @@
// src/views/Community/components/PopularKeywords.js
import React, { useState, useEffect } from 'react';
import { Tag, Space, Button } from 'antd';
import { useNavigate } from 'react-router-dom';
import { RightOutlined } from '@ant-design/icons';
import { logger } from '../../../utils/logger';
// 使用相对路径,让 MSW 在开发环境可以拦截请求
const API_BASE_URL = '/concept-api';
// 获取域名前缀
const DOMAIN_PREFIX = process.env.NODE_ENV === 'production'
? ''
: 'https://valuefrontier.cn';
const PopularKeywords = ({ onKeywordClick, keywords: propKeywords }) => {
const [keywords, setKeywords] = useState([]);
const navigate = useNavigate();
// 加载热门概念涨幅前20
const loadPopularConcepts = async () => {
try {
const response = await fetch(`${API_BASE_URL}/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '', // 空查询获取所有概念
size: 20, // 获取前20个
page: 1,
sort_by: 'change_pct' // 按涨跌幅排序
})
});
const data = await response.json();
if (data.results) {
// 转换数据格式
const formattedData = data.results.map(item => ({
keyword: item.concept,
count: item.stock_count,
change_pct: item.price_info?.avg_change_pct || 0,
concept_id: item.concept_id
}));
setKeywords(formattedData);
logger.debug('PopularKeywords', '热门概念加载成功(自己请求)', {
count: formattedData.length
});
}
} catch (error) {
logger.error('PopularKeywords', 'loadPopularConcepts', error);
setKeywords([]);
}
};
// 处理从父组件传入的数据
useEffect(() => {
if (propKeywords && propKeywords.length > 0) {
// 使用父组件传入的数据
setKeywords(propKeywords);
logger.debug('PopularKeywords', '使用父组件传入的数据', {
count: propKeywords.length
});
} else {
// 没有 prop 数据,自己加载
loadPopularConcepts();
}
}, [propKeywords]);
// 根据涨跌幅获取标签颜色
const getTagColor = (changePct) => {
if (changePct > 5) return 'red';
if (changePct > 3) return 'volcano';
if (changePct > 1) return 'orange';
if (changePct > 0) return 'gold';
if (changePct === 0) return 'default';
if (changePct > -1) return 'lime';
if (changePct > -3) return 'green';
if (changePct > -5) return 'cyan';
return 'blue';
};
// 格式化涨跌幅显示
const formatChangePct = (pct) => {
if (pct === null || pct === undefined) return '';
const formatted = pct.toFixed(2);
if (pct > 0) return `+${formatted}%`;
return `${formatted}%`;
};
// ✅ 修复:处理概念标签点击
const handleConceptClick = (concept) => {
// 优先调用父组件传入的回调(用于搜索框显示和触发搜索)
if (onKeywordClick) {
onKeywordClick(concept.keyword);
logger.debug('PopularKeywords', '调用 onKeywordClick 回调', {
keyword: concept.keyword
});
} else {
// 如果没有回调,则跳转到对应概念的页面(原有行为)
const url = `${DOMAIN_PREFIX}/htmls/${encodeURIComponent(concept.keyword)}.html`;
window.open(url, '_blank');
logger.debug('PopularKeywords', '跳转到概念页面', {
keyword: concept.keyword,
url
});
}
};
// 处理"更多概念"按钮点击 - 跳转到概念中心
const handleMoreClick = () => {
navigate('/concepts');
};
return (
<>
{keywords && keywords.length > 0 && (
<div style={{ position: 'relative' }}>
<Space
size={[6, 6]}
wrap
style={{
alignItems: 'center',
maxHeight: '29px', // 约两行的高度 (每行约28-30px)
overflow: 'hidden',
paddingRight: '90px' // 为右侧按钮留出空间
}}
>
{/* 标题 */}
<span style={{
color: '#ff4d4f',
fontSize: 13,
fontWeight: 500,
marginRight: 4
}}>
热门概念:
</span>
{/* 所有标签 */}
{keywords.map((item, index) => (
<Tag
key={item.concept_id || `keyword-${index}`}
color={getTagColor(item.change_pct)}
style={{
cursor: 'pointer',
padding: '1px 6px',
fontSize: 12,
transition: 'all 0.3s',
margin: 0
}}
onClick={() => handleConceptClick(item)}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.05)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)';
e.currentTarget.style.boxShadow = 'none';
}}
>
<span>{item.keyword}</span>
<span style={{
marginLeft: 4,
fontWeight: 'bold'
}}>
{formatChangePct(item.change_pct)}
</span>
{/* <span style={{
marginLeft: 3,
fontSize: 10,
opacity: 0.75
}}> */}
{/* ({item.count}股) */}
{/* </span> */}
</Tag>
))}
</Space>
{/* 更多概念按钮 - 固定在第二行右侧 */}
<Button
type="link"
size="small"
onClick={handleMoreClick}
style={{
position: 'absolute',
top: 0,
right: 0,
fontSize: 12,
padding: '0 4px',
height: 'auto'
}}
>
更多概念 <RightOutlined style={{ fontSize: 10 }} />
</Button>
</div>
)}
</>
);
};
export default PopularKeywords;

View File

@@ -9,7 +9,6 @@ import {
import {
Box,
Container,
useColorModeValue,
useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';