Files
vf_react/src/views/Community/index.js

209 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/index.js
import React, { useEffect, useRef, lazy, Suspense } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import {
fetchPopularKeywords,
fetchHotEvents
} from '@/store/slices/communityDataSlice';
import {
Box,
Container,
useColorModeValue,
useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';
// 导入组件
import DynamicNewsCard from './components/DynamicNewsCard';
import HotEventsSection from './components/HotEventsSection';
// ⚡ HeroPanel 懒加载:包含 ECharts (~600KB),首屏不需要立即渲染
const HeroPanel = lazy(() => import('./components/HeroPanel'));
// 导入自定义 Hooks
import { useEventData } from './hooks/useEventData';
import { useEventFilters } from './hooks/useEventFilters';
import { useCommunityEvents } from './hooks/useCommunityEvents';
import { logger } from '@/utils/logger';
import { useNotification } from '@/contexts/NotificationContext';
import { PROFESSIONAL_COLORS } from '@/constants/professionalTheme';
import { flushPendingEventsBeforeUnload } from '@/utils/trackingHelpers';
// 导航栏已由 MainLayout 提供,无需在此导入
const Community = () => {
const navigate = useNavigate();
const location = useLocation(); // ⚡ 获取当前路由信息(用于判断是否在 /community 页面)
const dispatch = useDispatch();
// Redux状态
const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
// 专业配色 - 深色主题
const bgColor = PROFESSIONAL_COLORS.background.primary;
// Ref用于首次滚动到内容区域
const containerRef = useRef(null);
// 响应式容器宽度
const containerMaxW = useBreakpointValue({
base: '100%', // 移动端:全宽
sm: '100%', // 小屏:全宽
md: '100%', // 中屏:全宽
lg: '1200px', // 大屏1200px
xl: '1400px', // 超大屏1400px
});
// 响应式内边距
const containerPx = useBreakpointValue({
base: 2, // 移动端:最小内边距
sm: 3,
md: 4,
lg: 6,
});
// ⚡ 通知权限引导
const { browserPermission, requestBrowserPermission, registerEventUpdateCallback } = useNotification();
// ⚡ DynamicNewsCard 的 ref用于触发刷新
const dynamicNewsCardRef = useRef(null);
// 🎯 初始化Community埋点Hook
const communityEvents = useCommunityEvents({ navigate });
// 自定义 Hooks
const { filters, updateFilters, handlePageChange, handleEventClick, handleViewDetail } = useEventFilters({
navigate
});
const { events, pagination, loading, lastUpdateTime } = useEventData(filters);
// 加载热门关键词和热点事件(动态新闻由 DynamicNewsCard 内部管理)
useEffect(() => {
dispatch(fetchPopularKeywords());
dispatch(fetchHotEvents());
}, [dispatch]);
// ⚡ 页面卸载前刷新待发送的 PostHog 事件(性能优化)
useEffect(() => {
window.addEventListener('beforeunload', flushPendingEventsBeforeUnload);
return () => {
window.removeEventListener('beforeunload', flushPendingEventsBeforeUnload);
};
}, []);
// 🎯 追踪新闻列表查看(当事件列表加载完成后)
useEffect(() => {
if (events && events.length > 0 && !loading) {
communityEvents.trackNewsListViewed({
totalCount: pagination?.total || events.length,
sortBy: filters.sort,
importance: filters.importance,
dateRange: filters.date_range,
industryFilter: filters.industry_code,
});
}
}, [events, loading, pagination, filters]);
/**
* ⚡ 【核心逻辑】注册 Socket 新事件回调 - 当收到新事件时智能刷新列表
*
* 工作流程:
* 1. Socket 收到 'new_event' 事件 → NotificationContext 触发所有注册的回调
* 2. 本回调被触发 → 检查当前路由是否为 /community
* 3. 如果在 /community 页面 → 调用 DynamicNewsCard.refresh() 方法
* 4. DynamicNewsCard 根据模式和滚动位置决定是否刷新:
* - 纵向模式 + 第1页 → 刷新列表
* - 纵向模式 + 其他页 → 不刷新(避免打断用户)
* - 平铺模式 + 滚动在顶部 → 刷新列表
* - 平铺模式 + 滚动不在顶部 → 仅显示 Toast 提示
*
* 设计要点:
* - 使用 registerEventUpdateCallback 注册回调,返回的函数用于清理
* - 路由检查:只在 /community 页面触发刷新
* - 智能刷新:由 DynamicNewsCard 根据上下文决定刷新策略
* - 自动清理:组件卸载时自动注销回调
*/
useEffect(() => {
// 定义回调函数
const handleNewEvent = (eventData) => {
console.log('[Community] 🔔 收到新事件通知', {
currentPath: location.pathname,
eventData,
});
// 检查是否在 /community 页面
if (location.pathname === '/community') {
console.log('[Community] ✅ 当前在事件中心页面,触发 DynamicNewsCard 刷新');
// 调用 DynamicNewsCard 的 refresh 方法(智能刷新)
if (dynamicNewsCardRef.current) {
dynamicNewsCardRef.current.refresh();
} else {
console.warn('[Community] ⚠️ DynamicNewsCard ref 不可用,无法触发刷新');
}
} else {
console.log('[Community] ⏭️ 当前不在事件中心页面,跳过刷新', {
currentPath: location.pathname,
});
}
};
// 注册回调(返回清理函数)
const unregister = registerEventUpdateCallback(handleNewEvent);
console.log('[Community] ✅ 已注册 Socket 事件更新回调');
// 组件卸载时清理
return () => {
if (unregister) {
unregister();
console.log('[Community] 🧹 已注销 Socket 事件更新回调');
}
};
}, [location.pathname, registerEventUpdateCallback]); // 依赖路由变化重新注册
return (
<Box minH="100vh" bg={bgColor}>
{/* 主内容区域 */}
<Container ref={containerRef} maxW={containerMaxW} px={containerPx} pt={{ base: 3, md: 6 }} pb={{ base: 4, md: 8 }}>
{/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */}
<Suspense fallback={
<Box mb={6} p={4} borderRadius="xl" bg="rgba(255,255,255,0.02)">
<Skeleton height="200px" borderRadius="lg" startColor="gray.800" endColor="gray.700" />
</Box>
}>
<HeroPanel />
</Suspense>
{/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard
ref={dynamicNewsCardRef} // ⚡ 传递 ref用于触发刷新
filters={filters}
popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime}
onSearch={updateFilters}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
trackingFunctions={{
trackNewsArticleClicked: communityEvents.trackNewsArticleClicked,
trackNewsDetailOpened: communityEvents.trackNewsDetailOpened,
trackNewsFilterApplied: communityEvents.trackNewsFilterApplied,
trackNewsSorted: communityEvents.trackNewsSorted,
trackNewsSearched: communityEvents.trackNewsSearched,
trackRelatedStockClicked: communityEvents.trackRelatedStockClicked,
}}
/>
{/* 热点事件区域 - 移至底部 */}
<HotEventsSection
events={hotEvents}
onEventClick={communityEvents.trackNewsArticleClicked}
/>
</Container>
</Box>
);
};
export default Community;