From 1fc9f4790f3613b6e3cfd04d501f967b750b443d Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 19 Nov 2025 20:03:55 +0800 Subject: [PATCH] =?UTF-8?q?pref:=20=E6=B8=85=E7=90=86=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6.1 立即可删除(安全) 以下文件可以立即删除,不会影响任何功能: # 未使用的组件 src/views/Community/components/EventList.js src/views/Community/components/EventListSection.js src/views/Community/components/EventTimelineHeader.js src/views/Community/components/MarketReviewCard.js src/views/Community/components/UnifiedSearchBox.js src/views/Community/components/ImportanceLegend.js src/views/Community/components/IndustryCascader.js src/views/Community/components/EventDetailModal.js # 未使用的CSS src/views/Community/components/EventList.css # 备份文件 src/views/Community/components/EventList.js.bak # 测试文档 src/views/Community/components/DynamicNewsDetail/1.md 预计减少代码量:~2000行代码 --- .../Community/components/EventDetailModal.js | 283 ------ src/views/Community/components/EventList.css | 387 -------- src/views/Community/components/EventList.js | 490 ---------- .../Community/components/EventList.js.bak | 818 ---------------- .../Community/components/EventListSection.js | 75 -- .../components/EventTimelineHeader.js | 42 - .../Community/components/ImportanceLegend.js | 34 - .../Community/components/IndustryCascader.js | 78 -- .../Community/components/MarketReviewCard.js | 300 ------ .../Community/components/UnifiedSearchBox.js | 898 ------------------ 10 files changed, 3405 deletions(-) delete mode 100644 src/views/Community/components/EventDetailModal.js delete mode 100644 src/views/Community/components/EventList.css delete mode 100644 src/views/Community/components/EventList.js delete mode 100644 src/views/Community/components/EventList.js.bak delete mode 100644 src/views/Community/components/EventListSection.js delete mode 100644 src/views/Community/components/EventTimelineHeader.js delete mode 100644 src/views/Community/components/ImportanceLegend.js delete mode 100644 src/views/Community/components/IndustryCascader.js delete mode 100644 src/views/Community/components/MarketReviewCard.js delete mode 100644 src/views/Community/components/UnifiedSearchBox.js diff --git a/src/views/Community/components/EventDetailModal.js b/src/views/Community/components/EventDetailModal.js deleted file mode 100644 index 633de5c7..00000000 --- a/src/views/Community/components/EventDetailModal.js +++ /dev/null @@ -1,283 +0,0 @@ -// src/views/Community/components/EventDetailModal.js -import React, { useState, useEffect } from 'react'; -import { Modal, Spin, Descriptions, Tag, List, Badge, Empty, Input, Button, message } from 'antd'; -import { eventService } from '../../../services/eventService'; -import { logger } from '../../../utils/logger'; -import dayjs from 'dayjs'; - -const EventDetailModal = ({ visible, event, onClose }) => { - const [loading, setLoading] = useState(false); - const [eventDetail, setEventDetail] = useState(null); - const [commentText, setCommentText] = useState(''); - const [submitting, setSubmitting] = useState(false); - const [comments, setComments] = useState([]); - const [commentsLoading, setCommentsLoading] = useState(false); - - const loadEventDetail = async () => { - if (!event) return; - - setLoading(true); - try { - const response = await eventService.getEventDetail(event.id); - if (response.success) { - setEventDetail(response.data); - } - } catch (error) { - logger.error('EventDetailModal', 'loadEventDetail', error, { - eventId: event?.id - }); - } finally { - setLoading(false); - } - }; - - const loadComments = async () => { - if (!event) return; - - setCommentsLoading(true); - try { - // 使用统一的posts API获取评论 - const result = await eventService.getPosts(event.id); - if (result.success) { - setComments(result.data || []); - } - } catch (error) { - logger.error('EventDetailModal', 'loadComments', error, { - eventId: event?.id - }); - } finally { - setCommentsLoading(false); - } - }; - - useEffect(() => { - if (visible && event) { - loadEventDetail(); - loadComments(); - } - }, [visible, event]); - - const getImportanceColor = (importance) => { - const colors = { - S: 'red', - A: 'orange', - B: 'blue', - C: 'green' - }; - return colors[importance] || 'default'; - }; - - const getRelationDesc = (relationDesc) => { - // 处理空值 - if (!relationDesc) return ''; - - // 如果是字符串,直接返回 - if (typeof relationDesc === 'string') { - return relationDesc; - } - - // 如果是对象且包含data数组 - if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) { - const firstItem = relationDesc.data[0]; - if (firstItem) { - // 优先使用 query_part,其次使用 sentences - return firstItem.query_part || firstItem.sentences || ''; - } - } - - // 其他情况返回空字符串 - return ''; - }; - - const renderPriceTag = (value, label) => { - if (value === null || value === undefined) return `${label}: --`; - - const color = value > 0 ? '#ff4d4f' : '#52c41a'; - const prefix = value > 0 ? '+' : ''; - - return ( - - {label}: {prefix}{value.toFixed(2)}% - - ); - }; - - const handleSubmitComment = async () => { - if (!commentText.trim()) { - message.warning('请输入评论内容'); - return; - } - setSubmitting(true); - try { - // 使用统一的createPost API - const result = await eventService.createPost(event.id, { - content: commentText.trim(), - content_type: 'text' - }); - - if (result.success) { - message.success('评论发布成功'); - setCommentText(''); - // 重新加载评论列表 - loadComments(); - } else { - throw new Error(result.message || '评论失败'); - } - } catch (e) { - message.error(e.message || '评论失败'); - } finally { - setSubmitting(false); - } - }; - - return ( - - - {eventDetail && ( - <> - - - {dayjs(eventDetail.created_at).format('YYYY-MM-DD HH:mm:ss')} - - - {eventDetail.creator?.username || 'Anonymous'} - - - - - - {eventDetail.view_count || 0} - - - {renderPriceTag(eventDetail.related_avg_chg, '平均涨幅')} - {renderPriceTag(eventDetail.related_max_chg, '最大涨幅')} - {renderPriceTag(eventDetail.related_week_chg, '周涨幅')} - - - {eventDetail.description}(AI合成) - - - - {eventDetail.keywords && eventDetail.keywords.length > 0 && ( -
-

相关概念

- {eventDetail.keywords.map((keyword, index) => ( - - {keyword} - - ))} -
- )} - - {eventDetail.related_stocks && eventDetail.related_stocks.length > 0 && ( -
-

相关股票

- ( - { - const stockCode = stock.stock_code.split('.')[0]; - window.open(`https://valuefrontier.cn/company?scode=${stockCode}`, '_blank'); - }} - > - 股票详情 - - ]} - > - - {stock.change !== null && ( - 0 ? 'red' : 'green'}> - {stock.change > 0 ? '+' : ''}{stock.change.toFixed(2)}% - - )} - - )} - /> -
- )} - - {/* 讨论区 */} -
-

讨论区

- - {/* 评论列表 */} -
- - {comments.length === 0 ? ( - - ) : ( - ( - - - {comment.author?.username || 'Anonymous'} - - {dayjs(comment.created_at).format('MM-DD HH:mm')} - -
- } - description={ -
- {comment.content} -
- } - /> - - )} - /> - )} - -
- - {/* 评论输入框(登录后可用,未登录后端会返回401) */} -
-

发表评论

- setCommentText(e.target.value)} - maxLength={500} - showCount - /> -
- -
-
- - - )} -
-
- ); -}; - -export default EventDetailModal; \ No newline at end of file diff --git a/src/views/Community/components/EventList.css b/src/views/Community/components/EventList.css deleted file mode 100644 index a62d8b24..00000000 --- a/src/views/Community/components/EventList.css +++ /dev/null @@ -1,387 +0,0 @@ -/* src/views/Community/components/EventList.css */ - -/* 时间轴容器样式 */ -.event-timeline { - padding: 0 0 0 24px; -} - -/* 时间轴圆点样式 */ -.timeline-dot { - border: none !important; - cursor: pointer; - transition: all 0.3s; -} - -/* 时间轴事件卡片 */ -.timeline-event-card { - padding: 20px; - margin-bottom: 16px; - background: #fff; - border-radius: 12px; - border: 1px solid #f0f0f0; - cursor: pointer; - transition: all 0.3s; - position: relative; - overflow: hidden; -} - -.timeline-event-card:hover { - transform: translateX(8px); - box-shadow: 0 6px 20px rgba(0,0,0,0.08); - border-color: #d9d9d9; -} - -/* 重要性标记线 */ -.importance-marker { - position: absolute; - left: 0; - top: 0; - width: 4px; - height: 100%; - transition: width 0.3s; -} - -.timeline-event-card:hover .importance-marker { - width: 6px; -} - -/* 事件标题 */ -.event-title { - margin-bottom: 8px; -} - -.event-title a { - color: #1890ff; - font-size: 16px; - font-weight: 500; - text-decoration: none; - transition: color 0.3s; -} - -.event-title a:hover { - color: #40a9ff; - text-decoration: underline; -} - -/* 事件元信息 */ -.event-meta { - font-size: 12px; - color: #999; - margin-bottom: 12px; - display: flex; - align-items: center; - gap: 16px; -} - -.event-meta .anticon { - margin-right: 4px; -} - -.event-meta .separator { - margin: 0; - color: #e8e8e8; -} - -/* 事件描述 */ -.event-description { - margin: 0 0 12px; - color: #666; - line-height: 1.6; - font-size: 14px; -} - -/* 事件统计标签 */ -.event-stats { - margin-bottom: 16px; -} - -.event-stats .ant-tag { - border-radius: 4px; - padding: 2px 8px; - font-size: 12px; -} - -/* 事件操作区域 */ -.event-actions { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - color: #999; -} - -.event-actions > span { - margin-right: 0; -} - -.event-actions .anticon { - margin-right: 4px; -} - -/* 事件按钮 */ -.event-buttons { - margin-left: auto; -} - -.event-buttons .ant-btn { - font-size: 13px; -} - -.event-buttons .ant-btn-sm { - height: 28px; - padding: 0 12px; -} - -/* 重要性指示器 */ -.importance-indicator { - text-align: right; - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} - -.importance-indicator .ant-badge { - display: block; -} - -.importance-indicator .ant-avatar { - transition: all 0.3s; -} - -.timeline-event-card:hover .importance-indicator .ant-avatar { - transform: scale(1.1); -} - -.importance-label { - font-size: 12px; - color: #666; - font-weight: 500; -} - -/* 分页容器 */ -.pagination-container { - margin-top: 32px; - text-align: center; - padding-top: 24px; - border-top: 1px solid #f0f0f0; -} - -/* 空状态 */ -.empty-state { - text-align: center; - padding: 60px 0; - color: #999; - font-size: 14px; -} - -/* 响应式设计 */ -@media (max-width: 768px) { - .event-timeline { - padding-left: 0; - } - - .timeline-event-card { - padding: 16px; - } - - .event-title a { - font-size: 15px; - } - - .event-description { - font-size: 13px; - } - - .event-actions { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } - - .event-buttons { - margin-left: 0; - width: 100%; - } - - .event-buttons .ant-space { - width: 100%; - } - - .event-buttons .ant-btn { - flex: 1; - } - - .importance-indicator { - position: absolute; - top: 16px; - right: 16px; - } - - .importance-label { - display: none; - } -} - -/* 深色主题支持(可选) */ -@media (prefers-color-scheme: dark) { - .timeline-event-card { - background: #1f1f1f; - border-color: #303030; - } - - .timeline-event-card:hover { - border-color: #434343; - } - - .event-title a { - color: #4096ff; - } - - .event-description { - color: #bfbfbf; - } - - .event-meta { - color: #8c8c8c; - } -} - -/* 动画效果 */ -@keyframes fadeInLeft { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -/* 时间轴项目动画 */ -.ant-timeline-item { - animation: fadeInLeft 0.5s ease-out forwards; - opacity: 0; -} - -.ant-timeline-item:nth-child(1) { animation-delay: 0.1s; } -.ant-timeline-item:nth-child(2) { animation-delay: 0.2s; } -.ant-timeline-item:nth-child(3) { animation-delay: 0.3s; } -.ant-timeline-item:nth-child(4) { animation-delay: 0.4s; } -.ant-timeline-item:nth-child(5) { animation-delay: 0.5s; } -.ant-timeline-item:nth-child(6) { animation-delay: 0.6s; } -.ant-timeline-item:nth-child(7) { animation-delay: 0.7s; } -.ant-timeline-item:nth-child(8) { animation-delay: 0.8s; } -.ant-timeline-item:nth-child(9) { animation-delay: 0.9s; } -.ant-timeline-item:nth-child(10) { animation-delay: 1s; } - -/* 时间轴连接线样式 */ -.ant-timeline-item-tail { - border-left-style: dashed; - border-left-width: 2px; -} - -/* 涨跌幅标签特殊样式 */ -.event-stats .ant-tag[color="#ff4d4f"] { - background-color: #fff1f0; - border-color: #ffccc7; -} - -.event-stats .ant-tag[color="#52c41a"] { - background-color: #f6ffed; - border-color: #b7eb8f; -} - -/* 快速查看和详细信息按钮悬停效果 */ -.event-buttons .ant-btn-default:hover { - color: #40a9ff; - border-color: #40a9ff; -} - -.event-buttons .ant-btn-primary { - background: #1890ff; - border-color: #1890ff; -} - -.event-buttons .ant-btn-primary:hover { - background: #40a9ff; - border-color: #40a9ff; -} - -/* 工具提示样式 */ -.ant-tooltip-inner { - font-size: 12px; -} - -/* 徽章计数样式 */ -.importance-indicator .ant-badge-count { - font-size: 12px; - font-weight: bold; - min-width: 20px; - height: 20px; - line-height: 20px; - border-radius: 10px; - padding: 0 6px; -} - -/* 加载状态动画 */ -.timeline-event-card.loading { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -/* 特殊重要性等级样式增强 */ -.timeline-event-card[data-importance="S"] { - border-left: 4px solid #722ed1; -} - -.timeline-event-card[data-importance="A"] { - border-left: 4px solid #ff4d4f; -} - -.timeline-event-card[data-importance="B"] { - border-left: 4px solid #faad14; -} - -.timeline-event-card[data-importance="C"] { - border-left: 4px solid #52c41a; -} - -/* 时间轴左侧内容区域优化 */ -.ant-timeline-item-content { - padding-bottom: 0; - min-height: auto; -} - -/* 确保最后一个时间轴项目没有连接线 */ -.ant-timeline-item:last-child .ant-timeline-item-tail { - display: none; -} - -/* 打印样式优化 */ -@media print { - .timeline-event-card { - page-break-inside: avoid; - border: 1px solid #000; - } - - .event-buttons { - display: none; - } - - .importance-marker { - width: 2px !important; - background: #000 !important; - } -} \ No newline at end of file diff --git a/src/views/Community/components/EventList.js b/src/views/Community/components/EventList.js deleted file mode 100644 index 6e98a675..00000000 --- a/src/views/Community/components/EventList.js +++ /dev/null @@ -1,490 +0,0 @@ -// src/views/Community/components/EventList.js -import React, { useState, useEffect } from 'react'; -import { - Box, - VStack, - HStack, - Text, - Button, - Badge, - Flex, - Container, - useColorModeValue, - Switch, - FormControl, - FormLabel, - useToast, - Center, - Tooltip, -} from '@chakra-ui/react'; -import { InfoIcon } from '@chakra-ui/icons'; -import { useNavigate } from 'react-router-dom'; - -// 导入工具函数和常量 -import { logger } from '../../../utils/logger'; -import { getApiBase } from '../../../utils/apiConfig'; -import { useEventNotifications } from '../../../hooks/useEventNotifications'; -import { browserNotificationService } from '../../../services/browserNotificationService'; -import { useNotification } from '../../../contexts/NotificationContext'; -import { getImportanceConfig } from '../../../constants/importanceLevels'; - -// 导入子组件 -import EventCard from './EventCard'; - -// ========== 主组件 ========== -const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => { - const navigate = useNavigate(); - const toast = useToast(); - const [isCompactMode, setIsCompactMode] = useState(false); // 新增:紧凑模式状态 - const [followingMap, setFollowingMap] = useState({}); - const [followCountMap, setFollowCountMap] = useState({}); - const [localEvents, setLocalEvents] = useState(events); // 用于实时更新的本地事件列表 - - // 从 NotificationContext 获取推送权限相关状态和方法 - const { browserPermission, requestBrowserPermission } = useNotification(); - - // 实时事件推送集成 - const { isConnected } = useEventNotifications({ - eventType: 'all', - importance: 'all', - enabled: true, - onNewEvent: (event) => { - console.log('\n[EventList DEBUG] ========== EventList 收到新事件 =========='); - console.log('[EventList DEBUG] 事件数据:', event); - console.log('[EventList DEBUG] 事件 ID:', event?.id); - console.log('[EventList DEBUG] 事件标题:', event?.title); - logger.info('EventList', '收到新事件推送', event); - - // 发送浏览器原生通知 - console.log('[EventList DEBUG] 准备发送浏览器原生通知'); - console.log('[EventList DEBUG] 通知权限状态:', browserPermission); - if (browserPermission === 'granted') { - const importance = getImportanceConfig(event.importance); - const notification = browserNotificationService.sendNotification({ - title: `🔔 ${importance.label}级事件`, - body: event.title, - tag: `event_${event.id}`, - data: { - link: `/event-detail/${event.id}`, - eventId: event.id, - }, - autoClose: 10000, // 10秒后自动关闭 - }); - - if (notification) { - browserNotificationService.setupClickHandler(notification, navigate); - console.log('[EventList DEBUG] ✓ 浏览器原生通知已发送'); - } else { - console.log('[EventList DEBUG] ⚠️ 浏览器原生通知发送失败'); - } - } else { - console.log('[EventList DEBUG] ⚠️ 浏览器通知权限未授予,跳过原生通知'); - } - - console.log('[EventList DEBUG] 准备更新事件列表'); - // 将新事件添加到列表顶部(防止重复) - setLocalEvents((prevEvents) => { - console.log('[EventList DEBUG] 当前事件列表数量:', prevEvents.length); - const exists = prevEvents.some(e => e.id === event.id); - console.log('[EventList DEBUG] 事件是否已存在:', exists); - if (exists) { - logger.debug('EventList', '事件已存在,跳过添加', { eventId: event.id }); - console.log('[EventList DEBUG] ⚠️ 事件已存在,跳过添加'); - return prevEvents; - } - logger.info('EventList', '新事件添加到列表顶部', { eventId: event.id }); - console.log('[EventList DEBUG] ✓ 新事件添加到列表顶部'); - // 添加到顶部,最多保留 100 个 - const updatedEvents = [event, ...prevEvents].slice(0, 100); - console.log('[EventList DEBUG] 更新后事件列表数量:', updatedEvents.length); - return updatedEvents; - }); - console.log('[EventList DEBUG] ✓ 事件列表更新完成'); - console.log('[EventList DEBUG] ========== EventList 处理完成 ==========\n'); - } - }); - - // 同步外部 events 到 localEvents - useEffect(() => { - setLocalEvents(events); - }, [events]); - - // 初始化关注状态与计数 - useEffect(() => { - // 初始化计数映射 - const initCounts = {}; - localEvents.forEach(ev => { - initCounts[ev.id] = ev.follower_count || 0; - }); - setFollowCountMap(initCounts); - - const loadFollowing = async () => { - try { - const base = getApiBase(); - const res = await fetch(base + '/api/account/events/following', { credentials: 'include' }); - const data = await res.json(); - if (res.ok && data.success) { - const map = {}; - (data.data || []).forEach(ev => { map[ev.id] = true; }); - setFollowingMap(map); - logger.debug('EventList', '关注状态加载成功', { - followingCount: Object.keys(map).length - }); - } - } catch (e) { - logger.warn('EventList', '加载关注状态失败', { error: e.message }); - } - }; - loadFollowing(); - // 仅在 localEvents 更新时重跑 - }, [localEvents]); - - const toggleFollow = async (eventId) => { - try { - const base = getApiBase(); - const res = await fetch(base + `/api/events/${eventId}/follow`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include' - }); - const data = await res.json(); - if (!res.ok || !data.success) throw new Error(data.error || '操作失败'); - const isFollowing = data.data?.is_following; - const count = data.data?.follower_count ?? 0; - setFollowingMap(prev => ({ ...prev, [eventId]: isFollowing })); - setFollowCountMap(prev => ({ ...prev, [eventId]: count })); - logger.debug('EventList', '关注状态切换成功', { - eventId, - isFollowing, - followerCount: count - }); - } catch (e) { - logger.warn('EventList', '关注操作失败', { - eventId, - error: e.message - }); - } - }; - - // 处理推送开关切换 - const handlePushToggle = async (e) => { - const isChecked = e.target.checked; - - if (isChecked) { - // 用户想开启推送 - logger.info('EventList', '用户请求开启推送'); - const permission = await requestBrowserPermission(); - - if (permission === 'denied') { - // 权限被拒绝,显示设置指引 - logger.warn('EventList', '用户拒绝了推送权限'); - toast({ - title: '推送权限被拒绝', - description: '如需开启推送,请在浏览器设置中允许通知权限', - status: 'warning', - duration: 5000, - isClosable: true, - position: 'top', - }); - } else if (permission === 'granted') { - logger.info('EventList', '推送权限已授予'); - } - } else { - // 用户想关闭推送 - 提示需在浏览器设置中操作 - logger.info('EventList', '用户尝试关闭推送'); - toast({ - title: '关闭推送通知', - description: '如需关闭,请在浏览器设置中撤销通知权限', - status: 'info', - duration: 5000, - isClosable: true, - position: 'top', - }); - } - }; - - // 专业的金融配色方案 - const bgColor = useColorModeValue('gray.50', 'gray.900'); - const cardBg = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.700'); - const textColor = useColorModeValue('gray.700', 'gray.200'); - const mutedColor = useColorModeValue('gray.500', 'gray.400'); - const linkColor = useColorModeValue('blue.600', 'blue.400'); - const hoverBg = useColorModeValue('gray.50', 'gray.700'); - - - const handleTitleClick = (e, event) => { - e.preventDefault(); - e.stopPropagation(); - onEventClick(event); - }; - - const handleViewDetailClick = (e, eventId) => { - e.stopPropagation(); - navigate(`/event-detail/${eventId}`); - }; - - // 时间轴样式配置(固定使用轻量卡片样式) - const getTimelineBoxStyle = () => { - return { - bg: useColorModeValue('gray.50', 'gray.700'), - borderColor: useColorModeValue('gray.400', 'gray.500'), - borderWidth: '2px', - textColor: useColorModeValue('blue.600', 'blue.400'), - boxShadow: 'sm', - }; - }; - - // 分页组件 - const Pagination = ({ current, total, pageSize, onChange }) => { - const totalPages = Math.ceil(total / pageSize); - - // 计算要显示的页码数组(智能分页) - const getPageNumbers = () => { - const delta = 2; // 当前页左右各显示2个页码 - const range = []; - const rangeWithDots = []; - - // 始终显示第1页 - range.push(1); - - // 显示当前页附近的页码 - for (let i = current - delta; i <= current + delta; i++) { - if (i > 1 && i < totalPages) { - range.push(i); - } - } - - // 始终显示最后一页(如果总页数>1) - if (totalPages > 1) { - range.push(totalPages); - } - - // 去重并排序 - const uniqueRange = [...new Set(range)].sort((a, b) => a - b); - - // 添加省略号 - let prev = 0; - for (const page of uniqueRange) { - if (page - prev === 2) { - // 如果只差一个页码,直接显示 - rangeWithDots.push(prev + 1); - } else if (page - prev > 2) { - // 如果差距大于2,显示省略号 - rangeWithDots.push('...'); - } - rangeWithDots.push(page); - prev = page; - } - - return rangeWithDots; - }; - - const pageNumbers = getPageNumbers(); - - return ( - - - - - {pageNumbers.map((page, index) => { - if (page === '...') { - return ( - - ... - - ); - } - return ( - - ); - })} - - - - - - 共 {total} 条 - - - ); - }; - - return ( - - {/* 顶部控制栏:左空白 + 中间分页器 + 右侧控制(固定sticky) - 铺满全宽 */} - - - - {/* 左侧占位 */} - - - {/* 中间:分页器 */} - {pagination.total > 0 && localEvents.length > 0 ? ( - - - - 第 {pagination.current} / {Math.ceil(pagination.total / pagination.pageSize)} 页 - - - - 共 {pagination.total} 条 - - - ) : ( - - )} - - {/* 右侧:控制按钮 */} - - {/* WebSocket 连接状态 */} - - {isConnected ? '🟢 实时' : '🔴 离线'} - - - {/* 桌面推送开关 */} - - - 推送 - - - - - - - {/* 视图切换控制 */} - - - 精简 - - setIsCompactMode(e.target.checked)} - colorScheme="blue" - /> - - - - - - - {/* 事件列表内容 */} - - {localEvents.length > 0 ? ( - - {localEvents.map((event, index) => ( - - - - ))} - - ) : ( -
- - - - 暂无事件数据 - - -
- )} - - {pagination.total > 0 && ( - - )} -
-
- ); -}; - -export default EventList; \ No newline at end of file diff --git a/src/views/Community/components/EventList.js.bak b/src/views/Community/components/EventList.js.bak deleted file mode 100644 index 36a2f947..00000000 --- a/src/views/Community/components/EventList.js.bak +++ /dev/null @@ -1,818 +0,0 @@ -// src/views/Community/components/EventList.js -import React, { useState, useEffect } from 'react'; -import { - Box, - VStack, - HStack, - Text, - Button, - Badge, - Tag, - TagLabel, - TagLeftIcon, - Flex, - Avatar, - Tooltip, - IconButton, - Divider, - Container, - useColorModeValue, - Circle, - Stat, - StatNumber, - StatHelpText, - StatArrow, - ButtonGroup, - Heading, - SimpleGrid, - Card, - CardBody, - Center, - Link, - Spacer, - Switch, - FormControl, - FormLabel, -} from '@chakra-ui/react'; -import { - ViewIcon, - ChatIcon, - StarIcon, - TimeIcon, - InfoIcon, - WarningIcon, - WarningTwoIcon, - CheckCircleIcon, - TriangleUpIcon, - TriangleDownIcon, - ArrowForwardIcon, - ExternalLinkIcon, - ViewOffIcon, -} from '@chakra-ui/icons'; -import { useNavigate } from 'react-router-dom'; -import moment from 'moment'; -import { logger } from '../../../utils/logger'; - -// ========== 工具函数定义在组件外部 ========== -// 涨跌颜色配置(中国A股配色:红涨绿跌)- 分档次显示 -const getPriceChangeColor = (value) => { - if (value === null || value === undefined) return 'gray.500'; - - const absValue = Math.abs(value); - - if (value > 0) { - // 上涨用红色,根据涨幅大小使用不同深浅 - if (absValue >= 3) return 'red.600'; // 深红色:3%以上 - if (absValue >= 1) return 'red.500'; // 中红色:1-3% - return 'red.400'; // 浅红色:0-1% - } else if (value < 0) { - // 下跌用绿色,根据跌幅大小使用不同深浅 - if (absValue >= 3) return 'green.600'; // 深绿色:3%以上 - if (absValue >= 1) return 'green.500'; // 中绿色:1-3% - return 'green.400'; // 浅绿色:0-1% - } - return 'gray.500'; -}; - -const getPriceChangeBg = (value) => { - if (value === null || value === undefined) return 'gray.50'; - - const absValue = Math.abs(value); - - if (value > 0) { - // 上涨背景色 - if (absValue >= 3) return 'red.100'; // 深色背景:3%以上 - if (absValue >= 1) return 'red.50'; // 中色背景:1-3% - return 'red.50'; // 浅色背景:0-1% - } else if (value < 0) { - // 下跌背景色 - if (absValue >= 3) return 'green.100'; // 深色背景:3%以上 - if (absValue >= 1) return 'green.50'; // 中色背景:1-3% - return 'green.50'; // 浅色背景:0-1% - } - return 'gray.50'; -}; - -const getPriceChangeBorderColor = (value) => { - if (value === null || value === undefined) return 'gray.300'; - - const absValue = Math.abs(value); - - if (value > 0) { - // 上涨边框色 - if (absValue >= 3) return 'red.500'; // 深边框:3%以上 - if (absValue >= 1) return 'red.400'; // 中边框:1-3% - return 'red.300'; // 浅边框:0-1% - } else if (value < 0) { - // 下跌边框色 - if (absValue >= 3) return 'green.500'; // 深边框:3%以上 - if (absValue >= 1) return 'green.400'; // 中边框:1-3% - return 'green.300'; // 浅边框:0-1% - } - return 'gray.300'; -}; - -// 重要性等级配置 - 金融配色方案 -const importanceLevels = { - 'S': { - color: 'purple.600', - bgColor: 'purple.50', - borderColor: 'purple.200', - icon: WarningIcon, - label: '极高', - dotBg: 'purple.500', - }, - 'A': { - color: 'red.600', - bgColor: 'red.50', - borderColor: 'red.200', - icon: WarningTwoIcon, - label: '高', - dotBg: 'red.500', - }, - 'B': { - color: 'orange.600', - bgColor: 'orange.50', - borderColor: 'orange.200', - icon: InfoIcon, - label: '中', - dotBg: 'orange.500', - }, - 'C': { - color: 'green.600', - bgColor: 'green.50', - borderColor: 'green.200', - icon: CheckCircleIcon, - label: '低', - dotBg: 'green.500', - } -}; - -const getImportanceConfig = (importance) => { - return importanceLevels[importance] || importanceLevels['C']; -}; - -// 自定义的涨跌箭头组件(修复颜色问题) -const PriceArrow = ({ value }) => { - if (value === null || value === undefined) return null; - - const Icon = value > 0 ? TriangleUpIcon : TriangleDownIcon; - const color = value > 0 ? 'red.500' : 'green.500'; - - return ; -}; - -// ========== 主组件 ========== -const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => { - const navigate = useNavigate(); - const [isCompactMode, setIsCompactMode] = useState(false); // 新增:紧凑模式状态 - const [followingMap, setFollowingMap] = useState({}); - const [followCountMap, setFollowCountMap] = useState({}); - - // 初始化关注状态与计数 - useEffect(() => { - // 初始化计数映射 - const initCounts = {}; - events.forEach(ev => { - initCounts[ev.id] = ev.follower_count || 0; - }); - setFollowCountMap(initCounts); - - const loadFollowing = async () => { - try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); - const res = await fetch(base + '/api/account/events/following', { credentials: 'include' }); - const data = await res.json(); - if (res.ok && data.success) { - const map = {}; - (data.data || []).forEach(ev => { map[ev.id] = true; }); - setFollowingMap(map); - logger.debug('EventList', '关注状态加载成功', { - followingCount: Object.keys(map).length - }); - } - } catch (e) { - logger.warn('EventList', '加载关注状态失败', { error: e.message }); - } - }; - loadFollowing(); - // 仅在 events 更新时重跑 - }, [events]); - - const toggleFollow = async (eventId) => { - try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); - const res = await fetch(base + `/api/events/${eventId}/follow`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include' - }); - const data = await res.json(); - if (!res.ok || !data.success) throw new Error(data.error || '操作失败'); - const isFollowing = data.data?.is_following; - const count = data.data?.follower_count ?? 0; - setFollowingMap(prev => ({ ...prev, [eventId]: isFollowing })); - setFollowCountMap(prev => ({ ...prev, [eventId]: count })); - logger.debug('EventList', '关注状态切换成功', { - eventId, - isFollowing, - followerCount: count - }); - } catch (e) { - logger.warn('EventList', '关注操作失败', { - eventId, - error: e.message - }); - } - }; - - // 专业的金融配色方案 - const bgColor = useColorModeValue('gray.50', 'gray.900'); - const cardBg = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.700'); - const textColor = useColorModeValue('gray.700', 'gray.200'); - const mutedColor = useColorModeValue('gray.500', 'gray.400'); - const linkColor = useColorModeValue('blue.600', 'blue.400'); - const hoverBg = useColorModeValue('gray.50', 'gray.700'); - - const renderPriceChange = (value, label) => { - if (value === null || value === undefined) { - return ( - - {label}: -- - - ); - } - - 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 ( - - - - {label}: {isPositive ? '+' : ''}{value.toFixed(2)}% - - - ); - }; - - const handleTitleClick = (e, event) => { - e.preventDefault(); - e.stopPropagation(); - onEventClick(event); - }; - - const handleViewDetailClick = (e, eventId) => { - e.stopPropagation(); - navigate(`/event-detail/${eventId}`); - }; - - // 精简模式的事件渲染 - const renderCompactEvent = (event) => { - const importance = getImportanceConfig(event.importance); - const isFollowing = !!followingMap[event.id]; - const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); - - return ( - - {/* 时间线和重要性标记 */} - - - {event.importance || 'C'} - - - - - {/* 精简事件卡片 */} - onEventClick(event)} - mb={3} - > - - - {/* 左侧:标题和时间 */} - - handleTitleClick(e, event)} - cursor="pointer" - noOfLines={1} - > - {event.title} - - - - {moment(event.created_at).format('MM-DD HH:mm')} - - {event.creator?.username || 'Anonymous'} - - - - {/* 右侧:涨跌幅指标 */} - - - - - - - {event.related_avg_chg != null - ? `${event.related_avg_chg > 0 ? '+' : ''}${event.related_avg_chg.toFixed(2)}%` - : '--'} - - - - - - - - - - - - - ); - }; - - // 详细模式的事件渲染(原有的渲染方式,但修复了箭头颜色) - const renderDetailedEvent = (event) => { - const importance = getImportanceConfig(event.importance); - const isFollowing = !!followingMap[event.id]; - const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); - - return ( - - {/* 时间线和重要性标记 */} - - - {event.importance || 'C'} - - - - - {/* 事件卡片 */} - onEventClick(event)} - mb={4} - > - - - {/* 标题和重要性标签 */} - - - handleTitleClick(e, event)} - cursor="pointer" - > - {event.title} - - - - {importance.label}优先级 - - - - {/* 元信息 */} - - - - {moment(event.created_at).format('YYYY-MM-DD HH:mm')} - - - {event.creator?.username || 'Anonymous'} - - - {/* 描述 */} - - {event.description} - - - {/* 价格变化指标 */} - - - - - - - 平均涨幅 - - - {event.related_avg_chg != null ? ( - - - - {event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}% - - - ) : ( - -- - )} - - - - - - - - - - 最大涨幅 - - - {event.related_max_chg != null ? ( - - - - {event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg.toFixed(2)}% - - - ) : ( - -- - )} - - - - - - - - - - 周涨幅 - - - {event.related_week_chg != null ? ( - - - - {event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg.toFixed(2)}% - - - ) : ( - -- - )} - - - - - - - - - - {/* 统计信息和操作按钮 */} - - - - - - {event.view_count || 0} - - - - - - {event.post_count || 0} - - - - - - {followerCount} - - - - - - - - - - - - - - - ); - }; - - // 分页组件 - const Pagination = ({ current, total, pageSize, onChange }) => { - const totalPages = Math.ceil(total / pageSize); - - return ( - - - - - {[...Array(Math.min(5, totalPages))].map((_, i) => { - const pageNum = i + 1; - return ( - - ); - })} - {totalPages > 5 && ...} - {totalPages > 5 && ( - - )} - - - - - - 共 {total} 条 - - - ); - }; - - return ( - - - {/* 视图切换控制 */} - - - - 精简模式 - - setIsCompactMode(e.target.checked)} - colorScheme="blue" - /> - - - - {events.length > 0 ? ( - - {events.map((event, index) => ( - - {isCompactMode - ? renderCompactEvent(event) - : renderDetailedEvent(event) - } - - ))} - - ) : ( -
- - - - 暂无事件数据 - - -
- )} - - {pagination.total > 0 && ( - - )} -
-
- ); -}; - -export default EventList; \ No newline at end of file diff --git a/src/views/Community/components/EventListSection.js b/src/views/Community/components/EventListSection.js deleted file mode 100644 index 9a17759e..00000000 --- a/src/views/Community/components/EventListSection.js +++ /dev/null @@ -1,75 +0,0 @@ -// src/views/Community/components/EventListSection.js -// 事件列表区域组件(包含Loading、Empty、List三种状态) - -import React from 'react'; -import { - Box, - Center, - VStack, - Spinner, - Text -} from '@chakra-ui/react'; -import EventList from './EventList'; - -/** - * 事件列表区域组件 - * @param {boolean} loading - 加载状态 - * @param {Array} events - 事件列表 - * @param {Object} pagination - 分页信息 - * @param {Function} onPageChange - 分页变化回调 - * @param {Function} onEventClick - 事件点击回调 - * @param {Function} onViewDetail - 查看详情回调 - */ -const EventListSection = ({ - loading, - events, - pagination, - onPageChange, - onEventClick, - onViewDetail -}) => { - // ✅ 最小高度,避免加载后高度突变 - const minHeight = '600px'; - - // Loading 状态 - if (loading) { - return ( - -
- - - 正在加载最新事件... - -
-
- ); - } - - // Empty 状态 - if (!events || events.length === 0) { - return ( - -
- - 暂无事件数据 - -
-
- ); - } - - // List 状态 - return ( - - - - ); -}; - -export default EventListSection; diff --git a/src/views/Community/components/EventTimelineHeader.js b/src/views/Community/components/EventTimelineHeader.js deleted file mode 100644 index 4ec577bd..00000000 --- a/src/views/Community/components/EventTimelineHeader.js +++ /dev/null @@ -1,42 +0,0 @@ -// src/views/Community/components/EventTimelineHeader.js -// 事件时间轴标题组件 - -import React from 'react'; -import { - Flex, - VStack, - HStack, - Heading, - Text, - Badge -} from '@chakra-ui/react'; -import { TimeIcon } from '@chakra-ui/icons'; - -/** - * 事件时间轴标题组件 - * @param {Date} lastUpdateTime - 最后更新时间 - */ -const EventTimelineHeader = ({ lastUpdateTime }) => { - return ( - - - - - - 实时事件 - - - - 全网监控 - 智能捕获 - 深度分析 - - - - 最后更新: {lastUpdateTime.toLocaleTimeString()} - - - ); -}; - -export default EventTimelineHeader; diff --git a/src/views/Community/components/ImportanceLegend.js b/src/views/Community/components/ImportanceLegend.js deleted file mode 100644 index 7e3bfc5c..00000000 --- a/src/views/Community/components/ImportanceLegend.js +++ /dev/null @@ -1,34 +0,0 @@ -// src/views/Community/components/ImportanceLegend.js -import React from 'react'; -import { Card, Space, Badge } from 'antd'; - -const ImportanceLegend = () => { - const levels = [ - { level: 'S', color: '#ff4d4f', description: '重大事件,市场影响深远' }, - { level: 'A', color: '#faad14', description: '重要事件,影响较大' }, - { level: 'B', color: '#1890ff', description: '普通事件,有一定影响' }, - { level: 'C', color: '#52c41a', description: '参考事件,影响有限' } - ]; - - return ( - - - {levels.map(item => ( -
- - {item.level}级 - {item.description} - - } - /> -
- ))} -
-
- ); -}; - -export default ImportanceLegend; \ No newline at end of file diff --git a/src/views/Community/components/IndustryCascader.js b/src/views/Community/components/IndustryCascader.js deleted file mode 100644 index d5f0f41b..00000000 --- a/src/views/Community/components/IndustryCascader.js +++ /dev/null @@ -1,78 +0,0 @@ -// src/views/Community/components/IndustryCascader.js -import React, { useState, useCallback } from 'react'; -import { Card, Form, Cascader } from 'antd'; -import { useSelector, useDispatch } from 'react-redux'; -import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice'; -import { logger } from '../../../utils/logger'; - -const IndustryCascader = ({ onFilterChange, loading }) => { - const [industryCascaderValue, setIndustryCascaderValue] = useState([]); - - // 使用 Redux 获取行业数据 - const dispatch = useDispatch(); - const industryData = useSelector(selectIndustryData); - const industryLoading = useSelector(selectIndustryLoading); - - // Cascader 获得焦点时加载数据 - const handleCascaderFocus = useCallback(async () => { - if (!industryData || industryData.length === 0) { - logger.debug('IndustryCascader', 'Cascader 获得焦点,开始加载行业数据'); - await dispatch(fetchIndustryData()); - } - }, [dispatch, industryData]); - - // Cascader 选择变化 - const handleIndustryCascaderChange = (value, selectedOptions) => { - setIndustryCascaderValue(value); - - if (value && value.length > 0) { - // value[0] = 分类体系名称 - // value[1...n] = 行业代码(一级~四级) - const industryCode = value[value.length - 1]; // 最后一级的 code - const classification = value[0]; // 分类体系名称 - - onFilterChange('industry_classification', classification); - onFilterChange('industry_code', industryCode); - - logger.debug('IndustryCascader', 'Cascader 选择变化', { - value, - classification, - industryCode, - path: selectedOptions.map(o => o.label).join(' > ') - }); - } else { - // 清空 - onFilterChange('industry_classification', ''); - onFilterChange('industry_code', ''); - } - }; - - return ( - -
- - labels.join(' > ')} - showSearch={{ - filter: (inputValue, path) => - path.some(option => option.label.toLowerCase().includes(inputValue.toLowerCase())) - }} - style={{ width: '100%' }} - /> - -
-
- ); -}; - -export default IndustryCascader; diff --git a/src/views/Community/components/MarketReviewCard.js b/src/views/Community/components/MarketReviewCard.js deleted file mode 100644 index 77164be4..00000000 --- a/src/views/Community/components/MarketReviewCard.js +++ /dev/null @@ -1,300 +0,0 @@ -// src/views/Community/components/MarketReviewCard.js -// 市场复盘组件(左右布局:事件列表 | 事件详情) - -import React, { forwardRef, useState } from 'react'; -import { - Card, - CardHeader, - CardBody, - Box, - Flex, - VStack, - HStack, - Heading, - Text, - Badge, - Center, - Spinner, - useColorModeValue, - Grid, - GridItem, -} from '@chakra-ui/react'; -import { TimeIcon, InfoIcon } from '@chakra-ui/icons'; -import dayjs from 'dayjs'; -import CompactEventCard from './EventCard/CompactEventCard'; -import EventHeader from './EventCard/EventHeader'; -import EventStats from './EventCard/EventStats'; -import EventFollowButton from './EventCard/EventFollowButton'; -import EventPriceDisplay from './EventCard/EventPriceDisplay'; -import EventDescription from './EventCard/EventDescription'; -import { getImportanceConfig } from '../../../constants/importanceLevels'; - -/** - * 市场复盘 - 左右布局卡片组件 - * @param {Array} events - 事件列表 - * @param {boolean} loading - 加载状态 - * @param {Date} lastUpdateTime - 最后更新时间 - * @param {Function} onEventClick - 事件点击回调 - * @param {Function} onViewDetail - 查看详情回调 - * @param {Function} onToggleFollow - 切换关注回调 - * @param {Object} ref - 用于滚动的ref - */ -const MarketReviewCard = forwardRef(({ - events, - loading, - lastUpdateTime, - onEventClick, - onViewDetail, - onToggleFollow, - ...rest -}, ref) => { - const cardBg = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.700'); - const linkColor = useColorModeValue('blue.600', 'blue.400'); - const mutedColor = useColorModeValue('gray.500', 'gray.400'); - const textColor = useColorModeValue('gray.700', 'gray.200'); - const selectedBg = useColorModeValue('blue.50', 'blue.900'); - - // 选中的事件 - const [selectedEvent, setSelectedEvent] = useState(null); - - // 时间轴样式配置 - const getTimelineBoxStyle = () => { - return { - bg: useColorModeValue('gray.50', 'gray.700'), - borderColor: useColorModeValue('gray.400', 'gray.500'), - borderWidth: '2px', - textColor: useColorModeValue('blue.600', 'blue.400'), - boxShadow: 'sm', - }; - }; - - // 处理事件点击 - const handleEventClick = (event) => { - setSelectedEvent(event); - if (onEventClick) { - onEventClick(event); - } - }; - - // 渲染右侧事件详情 - const renderEventDetail = () => { - if (!selectedEvent) { - return ( -
- - - - 请从左侧选择事件查看详情 - - -
- ); - } - - const importance = getImportanceConfig(selectedEvent.importance); - - return ( - - - - {/* 第一行:标题+优先级 | 统计+关注 */} - - {/* 左侧:标题 + 优先级标签 */} - { - e.preventDefault(); - e.stopPropagation(); - if (onViewDetail) { - onViewDetail(e, selectedEvent.id); - } - }} - linkColor={linkColor} - compact={false} - size="lg" - /> - - {/* 右侧:统计数据 + 关注按钮 */} - - {/* 统计数据 */} - - - {/* 关注按钮 */} - onToggleFollow && onToggleFollow(selectedEvent.id)} - size="sm" - showCount={false} - /> - - - - {/* 第二行:价格标签 | 时间+作者 */} - - {/* 左侧:价格标签 */} - - - {/* 右侧:时间 + 作者 */} - - - {dayjs(selectedEvent.created_at).format('YYYY-MM-DD HH:mm')} - - - @{selectedEvent.creator?.username || 'Anonymous'} - - - - {/* 第三行:描述文字 */} - - - - - ); - }; - - return ( - - {/* 标题部分 */} - - - - - - - 市场复盘 - - - - 复盘 - 总结 - 完整 - - - - 最后更新: {lastUpdateTime?.toLocaleTimeString() || '未知'} - - - - - {/* 主体内容 */} - - {/* Loading 状态 */} - {loading && ( -
- - - 正在加载复盘数据... - -
- )} - - {/* Empty 状态 */} - {!loading && (!events || events.length === 0) && ( -
- - 暂无复盘数据 - -
- )} - - {/* 左右布局:事件列表 | 事件详情 */} - {!loading && events && events.length > 0 && ( - - {/* 左侧:事件列表 (33.3%) */} - - - - {events.map((event, index) => ( - handleEventClick(event)} - cursor="pointer" - bg={selectedEvent?.id === event.id ? selectedBg : 'transparent'} - borderRadius="md" - transition="all 0.2s" - _hover={{ bg: selectedBg }} - > - handleEventClick(event)} - onTitleClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleEventClick(event); - }} - onViewDetail={onViewDetail} - onToggleFollow={() => {}} - timelineStyle={getTimelineBoxStyle()} - borderColor={borderColor} - /> - - ))} - - - - - {/* 右侧:事件详情 (66.7%) */} - - {renderEventDetail()} - - - )} -
-
- ); -}); - -MarketReviewCard.displayName = 'MarketReviewCard'; - -export default MarketReviewCard; diff --git a/src/views/Community/components/UnifiedSearchBox.js b/src/views/Community/components/UnifiedSearchBox.js deleted file mode 100644 index a514ba2d..00000000 --- a/src/views/Community/components/UnifiedSearchBox.js +++ /dev/null @@ -1,898 +0,0 @@ -// src/views/Community/components/UnifiedSearchBox.js -// 搜索组件:三行布局(主搜索 + 热门概念 + 筛选区) -import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; -import { - Card, Input, Cascader, Button, Space, Tag, AutoComplete, Select as AntSelect -} from 'antd'; -import { - SearchOutlined, CloseCircleOutlined, StockOutlined -} from '@ant-design/icons'; -import dayjs from 'dayjs'; -import debounce from 'lodash/debounce'; -import { useSelector, useDispatch } from 'react-redux'; -import { fetchIndustryData, selectIndustryData, selectIndustryLoading } from '../../../store/slices/industrySlice'; -import { stockService } from '../../../services/stockService'; -import { logger } from '../../../utils/logger'; -import PopularKeywords from './PopularKeywords'; -import TradingTimeFilter from './TradingTimeFilter'; - -const { Option } = AntSelect; - -const UnifiedSearchBox = ({ - onSearch, - onSearchFocus, - popularKeywords = [], - filters = {}, - mode, // 显示模式(如:vertical, horizontal 等) - pageSize, // 每页显示数量 - trackingFunctions = {} // PostHog 追踪函数集合 -}) => { - - // 其他状态 - const [stockOptions, setStockOptions] = useState([]); // 股票下拉选项列表 - const [allStocks, setAllStocks] = useState([]); // 所有股票数据 - const [industryValue, setIndustryValue] = useState([]); - - // 筛选条件状态 - const [sort, setSort] = useState('new'); // 排序方式 - const [importance, setImportance] = useState([]); // 重要性(数组,支持多选) - const [tradingTimeRange, setTradingTimeRange] = useState(null); // 交易时段筛选 - - // ✅ 本地输入状态 - 管理用户的实时输入 - const [inputValue, setInputValue] = useState(''); - - // 使用 Redux 获取行业数据 - const dispatch = useDispatch(); - const industryData = useSelector(selectIndustryData); - const industryLoading = useSelector(selectIndustryLoading); - - // 加载行业数据函数 - const loadIndustryData = useCallback(() => { - if (!industryData) { - dispatch(fetchIndustryData()); - } - }, [dispatch, industryData]); - - // 搜索触发函数 - const triggerSearch = useCallback((params) => { - logger.debug('UnifiedSearchBox', '【5/5】✅ 最终触发搜索 - 调用onSearch回调', { - params: params, - timestamp: new Date().toISOString() - }); - onSearch(params); - }, [onSearch]); - - // ✅ 创建防抖的搜索函数(300ms 延迟) - const debouncedSearchRef = useRef(null); - - useEffect(() => { - // 创建防抖函数,使用 triggerSearch 而不是直接调用 onSearch - debouncedSearchRef.current = debounce((params) => { - logger.debug('UnifiedSearchBox', '⏱️ 防抖延迟结束,执行搜索', { - params: params, - delayMs: 300 - }); - triggerSearch(params); - }, 300); - - // 清理函数 - return () => { - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - }; - }, [triggerSearch]); - - // 加载所有股票数据 - useEffect(() => { - const loadStocks = async () => { - const response = await stockService.getAllStocks(); - if (response.success && response.data) { - setAllStocks(response.data); - logger.debug('UnifiedSearchBox', '股票数据加载成功', { - count: response.data.length - }); - } - }; - - loadStocks(); - }, []); - - // Cascader 获得焦点时加载数据 - const handleCascaderFocus = async () => { - if (!industryData || industryData.length === 0) { - logger.debug('UnifiedSearchBox', 'Cascader 获得焦点,开始加载行业数据'); - await loadIndustryData(); - } - }; - - // 从 props.filters 初始化所有内部状态 (只在组件首次挂载时执行) - // 辅助函数:递归查找行业代码的完整路径 - const findIndustryPath = React.useCallback((targetCode, data, currentPath = []) => { - if (!data || data.length === 0) return null; - - for (const item of data) { - const newPath = [...currentPath, item.value]; - - if (item.value === targetCode) { - return newPath; - } - - if (item.children && item.children.length > 0) { - const found = findIndustryPath(targetCode, item.children, newPath); - if (found) return found; - } - } - return null; - }, []); - - // ✅ 从 props.filters 初始化筛选条件和输入框值 - useEffect(() => { - if (!filters) return; - - // 初始化排序 - if (filters.sort) setSort(filters.sort); - - // 初始化重要性(字符串解析为数组) - if (filters.importance) { - const importanceArray = filters.importance === 'all' - ? [] // 'all' 对应空数组(不显示任何选中) - : filters.importance.split(',').map(v => v.trim()).filter(Boolean); - setImportance(importanceArray); - logger.debug('UnifiedSearchBox', '初始化重要性', { - filters_importance: filters.importance, - importanceArray - }); - } else { - setImportance([]); - } - - // ✅ 初始化行业分类(需要 industryData 加载完成) - // ⚠️ 只在 industryValue 为空时才从 filters 初始化,避免用户选择后被覆盖 - if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) { - const path = findIndustryPath(filters.industry_code, industryData); - if (path) { - setIndustryValue(path); - logger.debug('UnifiedSearchBox', '初始化行业分类', { - industry_code: filters.industry_code, - path - }); - } - } else if (!filters.industry_code && industryValue && industryValue.length > 0) { - // 如果 filters 中没有行业代码,但本地有值,清空本地值 - setIndustryValue([]); - logger.debug('UnifiedSearchBox', '清空行业分类(filters中无值)'); - } - - // ✅ 同步 filters.q 到输入框显示值 - if (filters.q) { - setInputValue(filters.q); - } else if (!filters.q) { - // 如果 filters 中没有搜索关键词,清空输入框 - setInputValue(''); - } - - // ✅ 初始化时间筛选(从 filters 中恢复) - // ⚠️ 只在 tradingTimeRange 为空时才从 filters 初始化,避免用户选择后被覆盖 - const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days; - - if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) { - // 根据参数推断按钮 key - let inferredKey = 'custom'; - let inferredLabel = ''; - - if (filters.recent_days) { - // 推断是否是预设按钮 - if (filters.recent_days === '7') { - inferredKey = 'week'; - inferredLabel = '近一周'; - } else if (filters.recent_days === '30') { - inferredKey = 'month'; - inferredLabel = '近一月'; - } else { - inferredLabel = `近${filters.recent_days}天`; - } - } else if (filters.start_date && filters.end_date) { - inferredLabel = `${dayjs(filters.start_date).format('MM-DD HH:mm')} - ${dayjs(filters.end_date).format('MM-DD HH:mm')}`; - } - - // 从 filters 重建 tradingTimeRange 状态 - const timeRange = { - start_date: filters.start_date || '', - end_date: filters.end_date || '', - recent_days: filters.recent_days || '', - label: inferredLabel, - key: inferredKey - }; - setTradingTimeRange(timeRange); - logger.debug('UnifiedSearchBox', '初始化时间筛选', { - filters_time: { - start_date: filters.start_date, - end_date: filters.end_date, - recent_days: filters.recent_days - }, - tradingTimeRange: timeRange - }); - } else if (!hasTimeInFilters && tradingTimeRange) { - // 如果 filters 中没有时间参数,但本地有值,清空本地值 - setTradingTimeRange(null); - logger.debug('UnifiedSearchBox', '清空时间筛选(filters中无值)'); - } - }, [filters.sort, filters.importance, filters.industry_code, filters.q, filters.start_date, filters.end_date, filters.recent_days, industryData, findIndustryPath, industryValue, tradingTimeRange]); - - // AutoComplete 搜索股票(模糊匹配 code 或 name) - const handleSearch = (value) => { - if (!value || !allStocks || allStocks.length === 0) { - setStockOptions([]); - return; - } - - // 使用 stockService 进行模糊搜索 - const results = stockService.fuzzySearch(value, allStocks, 10); - - // 转换为 AutoComplete 选项格式 - const options = results.map(stock => ({ - value: stock.code, - label: ( -
- - {stock.code} - {stock.name} -
- ), - // 保存完整的股票信息,用于选中后显示 - stockInfo: stock - })); - - setStockOptions(options); - logger.debug('UnifiedSearchBox', '股票模糊搜索', { - query: value, - resultCount: options.length - }); - }; - - // ✅ 选中股票(从下拉选择) - 更新输入框并触发搜索 - const handleStockSelect = (_value, option) => { - const stockInfo = option.stockInfo; - if (stockInfo) { - logger.debug('UnifiedSearchBox', '选中股票', { - code: stockInfo.code, - name: stockInfo.name - }); - - // 🎯 追踪股票点击 - if (trackingFunctions.trackRelatedStockClicked) { - trackingFunctions.trackRelatedStockClicked({ - stockCode: stockInfo.code, - stockName: stockInfo.name, - source: 'search_box_autocomplete', - timestamp: new Date().toISOString(), - }); - } - - // 更新输入框显示 - setInputValue(`${stockInfo.code} ${stockInfo.name}`); - - // 直接构建参数并触发搜索 - 使用股票代码作为 q 参数 - const params = buildFilterParams({ - q: stockInfo.code, // 使用股票代码作为搜索关键词 - industry_code: '' - }); - logger.debug('UnifiedSearchBox', '自动触发股票搜索', params); - triggerSearch(params); - } - }; - - // ✅ 重要性变化(立即执行)- 支持多选 - const handleImportanceChange = (value) => { - logger.debug('UnifiedSearchBox', '重要性值改变', { - oldValue: importance, - newValue: value - }); - - setImportance(value); - - // 取消之前的防抖搜索 - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - // 转换为逗号分隔字符串传给后端(空数组表示"全部") - const importanceStr = value.length === 0 ? 'all' : value.join(','); - - // 🎯 追踪筛选操作 - if (trackingFunctions.trackNewsFilterApplied) { - trackingFunctions.trackNewsFilterApplied({ - filterType: 'importance', - filterValue: importanceStr, - timestamp: new Date().toISOString(), - }); - } - - // 立即触发搜索 - const params = buildFilterParams({ importance: importanceStr }); - logger.debug('UnifiedSearchBox', '重要性改变,立即触发搜索', params); - - triggerSearch(params); - }; - - // ✅ 排序变化(立即触发搜索) - const handleSortChange = (value) => { - logger.debug('UnifiedSearchBox', '排序值改变', { - oldValue: sort, - newValue: value - }); - setSort(value); - - // 取消之前的防抖搜索 - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - // 🎯 追踪排序操作 - if (trackingFunctions.trackNewsSorted) { - trackingFunctions.trackNewsSorted({ - sortBy: value, - previousSortBy: sort, - timestamp: new Date().toISOString(), - }); - } - - // 立即触发搜索 - const params = buildFilterParams({ sort: value }); - logger.debug('UnifiedSearchBox', '排序改变,立即触发搜索', params); - triggerSearch(params); - }; - - // ✅ 行业分类变化(立即触发搜索) - const handleIndustryChange = (value) => { - logger.debug('UnifiedSearchBox', '行业分类值改变', { - oldValue: industryValue, - newValue: value - }); - setIndustryValue(value); - - // 取消之前的防抖搜索 - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - // 🎯 追踪行业筛选 - if (trackingFunctions.trackNewsFilterApplied) { - trackingFunctions.trackNewsFilterApplied({ - filterType: 'industry', - filterValue: value?.[value.length - 1] || '', - timestamp: new Date().toISOString(), - }); - } - - // 立即触发搜索 - const params = buildFilterParams({ - industry_code: value?.[value.length - 1] || '' - }); - logger.debug('UnifiedSearchBox', '行业改变,立即触发搜索', params); - - triggerSearch(params); - }; - - // ✅ 热门概念点击处理(立即搜索,不使用防抖) - 更新输入框并触发搜索 - const handleKeywordClick = (keyword) => { - // 更新输入框显示 - setInputValue(keyword); - - // 立即触发搜索(取消之前的防抖) - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - // 🎯 追踪热门关键词点击 - if (trackingFunctions.trackNewsSearched) { - trackingFunctions.trackNewsSearched({ - searchQuery: keyword, - searchType: 'popular_keyword', - timestamp: new Date().toISOString(), - }); - } - - const params = buildFilterParams({ - q: keyword, - industry_code: '' - }); - logger.debug('UnifiedSearchBox', '热门概念点击,立即触发搜索', { - keyword, - params - }); - triggerSearch(params); - }; - - // ✅ 交易时段筛选变化(立即触发搜索) - const handleTradingTimeChange = (timeConfig) => { - if (!timeConfig) { - // 清空筛选 - setTradingTimeRange(null); - - // 🎯 追踪时间筛选清空 - if (trackingFunctions.trackNewsFilterApplied) { - trackingFunctions.trackNewsFilterApplied({ - filterType: 'time_range', - filterValue: 'cleared', - timestamp: new Date().toISOString(), - }); - } - - const params = buildFilterParams({ - start_date: '', - end_date: '', - recent_days: '' - }); - triggerSearch(params); - return; - } - - const { range, type, label, key } = timeConfig; - let params = {}; - - if (type === 'recent_days') { - // 近一周/近一月使用 recent_days - params.recent_days = range; - params.start_date = ''; - params.end_date = ''; - } else { - // 其他使用 start_date + end_date - params.start_date = range[0].format('YYYY-MM-DD HH:mm:ss'); - params.end_date = range[1].format('YYYY-MM-DD HH:mm:ss'); - params.recent_days = ''; - } - - setTradingTimeRange({ ...params, label, key }); - - // 🎯 追踪时间筛选 - if (trackingFunctions.trackNewsFilterApplied) { - trackingFunctions.trackNewsFilterApplied({ - filterType: 'time_range', - filterValue: label, - timeRangeType: type, - timestamp: new Date().toISOString(), - }); - } - - // 立即触发搜索 - const searchParams = buildFilterParams({ ...params, mode }); - logger.debug('UnifiedSearchBox', '交易时段筛选变化,立即触发搜索', { - timeConfig, - params: searchParams - }); - triggerSearch(searchParams); - }; - - // 主搜索(点击搜索按钮或回车) - const handleMainSearch = () => { - // 取消之前的防抖 - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - // 构建参数并触发搜索 - 使用用户输入作为 q 参数 - const params = buildFilterParams({ - q: inputValue, // 使用用户输入(可能是话题、股票代码、股票名称等) - industry_code: '' - }); - - // 🎯 追踪搜索操作 - if (trackingFunctions.trackNewsSearched && inputValue) { - trackingFunctions.trackNewsSearched({ - searchQuery: inputValue, - searchType: 'main_search', - filters: params, - timestamp: new Date().toISOString(), - }); - } - - logger.debug('UnifiedSearchBox', '主搜索触发', { - inputValue, - params - }); - triggerSearch(params); - }; - - // ✅ 处理输入变化 - 更新本地输入状态 - const handleInputChange = (value) => { - logger.debug('UnifiedSearchBox', '输入变化', { value }); - setInputValue(value); - }; - - // ✅ 生成完整的筛选参数对象 - 直接从 filters 和本地筛选器状态构建 - const buildFilterParams = useCallback((overrides = {}) => { - logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输入参数', { - overrides: overrides, - currentState: { - sort, - importance, - industryValue, - 'filters.q': filters.q, - mode, - pageSize - } - }); - - // 处理排序参数 - 将 returns_avg/returns_week 转换为 sort=returns + return_type - const sortValue = overrides.sort ?? sort; - let actualSort = sortValue; - let returnType; - - if (sortValue === 'returns_avg') { - actualSort = 'returns'; - returnType = 'avg'; - } else if (sortValue === 'returns_week') { - actualSort = 'returns'; - returnType = 'week'; - } - - // 处理重要性参数:数组转换为逗号分隔字符串 - let importanceValue = overrides.importance ?? importance; - if (Array.isArray(importanceValue)) { - importanceValue = importanceValue.length === 0 - ? 'all' - : importanceValue.join(','); - } - - const result = { - // 基础参数(overrides 优先级高于本地状态) - sort: actualSort, - importance: importanceValue, - - - // 搜索参数: 统一使用 q 参数进行搜索(话题/股票/关键词) - q: (overrides.q ?? filters.q) ?? '', - // 行业代码: 取选中路径的最后一级(最具体的行业代码) - industry_code: overrides.industry_code ?? (industryValue?.[industryValue.length - 1] || ''), - - // 交易时段筛选参数 - start_date: overrides.start_date ?? (tradingTimeRange?.start_date || ''), - end_date: overrides.end_date ?? (tradingTimeRange?.end_date || ''), - recent_days: overrides.recent_days ?? (tradingTimeRange?.recent_days || ''), - - // 最终 overrides 具有最高优先级 - ...overrides, - page: 1, - per_page: overrides.mode === 'four-row' ? 30: 10 - }; - - // 删除可能来自 overrides 的旧 per_page 值(将由 pageSize 重新设置) - delete result.per_page; - - // 添加 return_type 参数(如果需要) - if (returnType) { - result.return_type = returnType; - } - - // 添加 mode 和 per_page 参数(如果提供了的话) - if (mode !== undefined && mode !== null) { - result.mode = mode; - } - if (pageSize !== undefined && pageSize !== null) { - result.per_page = pageSize; // 后端实际使用的参数 - } - - logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输出结果', result); - return result; - }, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]); - - // ✅ 重置筛选 - 清空所有筛选器并触发搜索 - const handleReset = () => { - console.log('%c🔄 [重置] 开始重置筛选条件', 'color: #FF4D4F; font-weight: bold;'); - - // 重置所有筛选器状态 - setInputValue(''); // 清空输入框 - setStockOptions([]); - setIndustryValue([]); - setSort('new'); - setImportance([]); // 改为空数组 - setTradingTimeRange(null); // 清空交易时段筛选 - - // 🎯 追踪筛选重置 - if (trackingFunctions.trackNewsFilterApplied) { - trackingFunctions.trackNewsFilterApplied({ - filterType: 'reset', - filterValue: 'all_filters_cleared', - timestamp: new Date().toISOString(), - }); - } - - // 输出重置后的完整参数 - const resetParams = { - q: '', - industry_code: '', - sort: 'new', - importance: 'all', // 传给后端时转为'all' - start_date: '', - end_date: '', - recent_days: '', - page: 1, - _forceRefresh: Date.now() // 添加强制刷新标志,确保每次重置都触发更新 - }; - - console.log('%c🔄 [重置] 重置参数', 'color: #FF4D4F;', resetParams); - logger.debug('UnifiedSearchBox', '重置筛选', resetParams); - - console.log('%c🔄 [重置] 调用 onSearch', 'color: #FF4D4F;', typeof onSearch); - onSearch(resetParams); - - console.log('%c✅ [重置] 重置完成', 'color: #52C41A; font-weight: bold;'); - }; - - // 生成已选条件标签(包含所有筛选条件) - 从 filters 和本地状态读取 - const filterTags = useMemo(() => { - const tags = []; - - // 搜索关键词标签 - 从 filters.q 读取 - if (filters.q) { - tags.push({ key: 'search', label: `搜索: ${filters.q}` }); - } - - // 行业标签 - if (industryValue && industryValue.length > 0 && industryData) { - // 递归查找每个层级的 label - const findLabel = (code, data) => { - for (const item of data) { - if (code.startsWith(item.value)) { - if (item.value === code) { - return item.label; - } else { - return findLabel(code, item.children); - } - } - } - return null; - }; - - // 只显示最后一级的 label - const lastLevelCode = industryValue[industryValue.length - 1]; - const lastLevelLabel = findLabel(lastLevelCode, industryData); - - tags.push({ - key: 'industry', - label: `行业: ${lastLevelLabel}` - }); - } - - // 交易时段筛选标签 - if (tradingTimeRange?.label) { - tags.push({ - key: 'trading_time', - label: `时间: ${tradingTimeRange.label}` - }); - } - - // 重要性标签(多选合并显示为单个标签) - if (importance && importance.length > 0) { - const importanceMap = { 'S': '极高', 'A': '高', 'B': '中', 'C': '低' }; - const importanceLabel = importance.map(imp => importanceMap[imp] || imp).join(', '); - tags.push({ key: 'importance', label: `重要性: ${importanceLabel}` }); - } - - // 排序标签(排除默认值 'new') - if (sort && sort !== 'new') { - let sortLabel; - if (sort === 'hot') sortLabel = '最热'; - else if (sort === 'importance') sortLabel = '重要性'; - else if (sort === 'returns_avg') sortLabel = '平均收益率'; - else if (sort === 'returns_week') sortLabel = '周收益率'; - else sortLabel = sort; - tags.push({ key: 'sort', label: `排序: ${sortLabel}` }); - } - - return tags; - }, [filters.q, industryValue, importance, sort, tradingTimeRange]); - - // ✅ 移除单个标签 - 构建新参数并触发搜索 - const handleRemoveTag = (key) => { - logger.debug('UnifiedSearchBox', '移除标签', { key }); - - // 取消所有待执行的防抖搜索(避免旧的防抖覆盖删除操作) - if (debouncedSearchRef.current) { - debouncedSearchRef.current.cancel(); - } - - if (key === 'search') { - // 清除搜索关键词和输入框,立即触发搜索 - setInputValue(''); // 清空输入框 - const params = buildFilterParams({ q: '' }); - logger.debug('UnifiedSearchBox', '移除搜索标签后触发搜索', { key, params }); - triggerSearch(params); - } else if (key === 'industry') { - // 清除行业选择 - setIndustryValue([]); - const params = buildFilterParams({ industry_code: '' }); - triggerSearch(params); - } else if (key === 'trading_time') { - // 清除交易时段筛选 - setTradingTimeRange(null); - const params = buildFilterParams({ - start_date: '', - end_date: '', - recent_days: '' - }); - triggerSearch(params); - } else if (key === 'importance') { - // 重置重要性为空数组(传给后端为'all') - setImportance([]); - const params = buildFilterParams({ importance: 'all' }); - triggerSearch(params); - } else if (key === 'sort') { - // 重置排序为默认值 - setSort('new'); - const params = buildFilterParams({ sort: 'new' }); - triggerSearch(params); - } - }; - - return ( -
- {/* 第三行:行业 + 重要性 + 排序 */} - - {/* 左侧:筛选器组 */} - - 筛选: - {/* 行业分类 */} - - path.some(option => - option.label.toLowerCase().includes(inputValue.toLowerCase()) - ) - }} - allowClear - expandTrigger="hover" - displayRender={(labels) => labels.join(' > ')} - disabled={industryLoading} - style={{ width: 160 }} - size="small" - /> - - {/* 重要性 */} - - 重要性: - - - - - - - - - {/* 搜索图标(可点击) + 搜索框 */} - - { - e.currentTarget.style.color = '#096dd9'; - e.currentTarget.style.background = '#bae7ff'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.color = '#1890ff'; - e.currentTarget.style.background = '#e6f7ff'; - }} - /> - { - if (e.key === 'Enter') { - handleMainSearch(); - } - }} - style={{ flex: 1 }} - size="small" - notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null} - /> - - - {/* 重置按钮 - 现代化设计 */} - - - - {/* 右侧:排序 */} - - 排序: - - - - - - - - - - - {/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */} - - 时间筛选: - - {/* 交易时段筛选 */} - - - - {/* 第二行:热门概念 */} -
- -
-
- ); -}; - -export default UnifiedSearchBox;