diff --git a/src/views/Community/components/DynamicNewsCard/PageNavigationButton.js b/src/views/Community/components/DynamicNewsCard/PageNavigationButton.js
deleted file mode 100644
index 723e89cd..00000000
--- a/src/views/Community/components/DynamicNewsCard/PageNavigationButton.js
+++ /dev/null
@@ -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 (
- }
- 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;
diff --git a/src/views/Community/components/DynamicNewsCard/hooks/useInfiniteScroll.js b/src/views/Community/components/DynamicNewsCard/hooks/useInfiniteScroll.js
deleted file mode 100644
index 17ccfb8c..00000000
--- a/src/views/Community/components/DynamicNewsCard/hooks/useInfiniteScroll.js
+++ /dev/null
@@ -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 };
-};
diff --git a/src/views/Community/components/EventDiscussionModal.js b/src/views/Community/components/EventDiscussionModal.js
deleted file mode 100644
index c5731937..00000000
--- a/src/views/Community/components/EventDiscussionModal.js
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
- {discussionType}
-
- {eventTitle && (
-
- {eventTitle}
-
- )}
-
-
-
-
-
- {/* 发布新帖子 */}
-
- setNewPostTitle(e.target.value)}
- placeholder="帖子标题(可选)"
- size="sm"
- mb={2}
- />
-
-
-
-
- {/* 帖子列表 */}
- {loading ? (
-
-
-
- ) : posts.length > 0 ? (
-
- {posts.map((post) => (
-
- {/* 帖子头部 */}
-
-
-
-
-
-
- {post.user?.username || '匿名用户'}
-
-
-
-
-
- {format(new Date(post.created_at), 'MM月dd日 HH:mm', {
- locale: zhCN,
- })}
-
-
-
-
-
- {/* 操作菜单 */}
-
-
-
- {/* 帖子标题 */}
- {post.title && (
-
- {post.title}
-
- )}
-
- {/* 帖子内容 */}
-
- {post.content}
-
-
- {/* 帖子操作栏 */}
-
- : }
- color={post.liked ? 'red.500' : 'gray.600'}
- onClick={() => handleLikePost(post.id)}
- >
- {post.likes_count || 0}
-
- }
- onClick={() => togglePostComments(post.id)}
- rightIcon={expandedPosts[post.id] ? : }
- >
- {post.comments_count || 0} 评论
-
-
-
- {/* 评论区 */}
-
-
- {/* 评论输入框 */}
-
-
-
- {/* 评论列表 */}
- {loadingComments[post.id] ? (
-
-
-
- ) : (
-
- {postComments[post.id]?.map((comment) => (
-
-
-
-
-
- {comment.user?.username || '匿名用户'}
-
-
- {format(new Date(comment.created_at), 'MM-dd HH:mm')}
-
-
- }
- variant="ghost"
- onClick={() => handleDeleteComment(comment.id, post.id)}
- />
-
-
- {comment.content}
-
-
- {/* 显示回复 */}
- {comment.replies && comment.replies.length > 0 && (
-
- {comment.replies.map((reply) => (
-
-
-
- {reply.user?.username}:
-
- {reply.content}
-
-
- ))}
-
- )}
-
- ))}
- {(!postComments[post.id] || postComments[post.id].length === 0) && (
-
- 暂无评论
-
- )}
-
- )}
-
-
-
- ))}
-
- ) : (
-
-
-
- 暂无帖子,快来发表您的观点吧!
-
-
- )}
-
-
-
- );
-};
-
-export default EventDiscussionModal;
diff --git a/src/views/Community/components/PopularKeywords.js b/src/views/Community/components/PopularKeywords.js
deleted file mode 100644
index 0b195ec7..00000000
--- a/src/views/Community/components/PopularKeywords.js
+++ /dev/null
@@ -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 && (
-
-
- {/* 标题 */}
-
- 热门概念:
-
-
- {/* 所有标签 */}
- {keywords.map((item, index) => (
- 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';
- }}
- >
- {item.keyword}
-
- {formatChangePct(item.change_pct)}
-
- {/* */}
- {/* ({item.count}股) */}
- {/* */}
-
- ))}
-
-
- {/* 更多概念按钮 - 固定在第二行右侧 */}
-
-
- )}
- >
- );
-};
-
-export default PopularKeywords;
\ No newline at end of file
diff --git a/src/views/Community/index.js b/src/views/Community/index.js
index 073ce724..ade776ea 100644
--- a/src/views/Community/index.js
+++ b/src/views/Community/index.js
@@ -9,7 +9,6 @@ import {
import {
Box,
Container,
- useColorModeValue,
useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';