核心改动: - 扩展 NotificationContext,添加事件更新回调注册机制 - VirtualizedFourRowGrid 添加 forwardRef 暴露 getScrollPosition 方法 - DynamicNewsCard 实现智能刷新逻辑(根据模式和滚动位置判断是否刷新) - Community 页面注册 Socket 回调自动触发刷新 - 创建 TypeScript 通用防抖工具函数(debounce.ts) - 集成防抖机制(2秒延迟),避免短时间内频繁请求 智能刷新策略: - 纵向模式 + 第1页:自动刷新列表 - 纵向模式 + 其他页:不刷新(避免打断用户) - 平铺模式 + 滚动在顶部:自动刷新列表 - 平铺模式 + 滚动不在顶部:仅显示 Toast 提示 防抖效果: - 短时间内收到多个新事件,只执行最后一次刷新 - 减少服务器压力,提升用户体验 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
297 lines
10 KiB
JavaScript
297 lines
10 KiB
JavaScript
// src/views/Community/index.js
|
||
import React, { useEffect, useRef, useState } 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,
|
||
Alert,
|
||
AlertIcon,
|
||
AlertTitle,
|
||
AlertDescription,
|
||
Button,
|
||
CloseButton,
|
||
HStack,
|
||
VStack,
|
||
Text,
|
||
useBreakpointValue,
|
||
} from '@chakra-ui/react';
|
||
|
||
// 导入组件
|
||
import DynamicNewsCard from './components/DynamicNewsCard';
|
||
import HotEventsSection from './components/HotEventsSection';
|
||
import HeroPanel from './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';
|
||
|
||
// 导航栏已由 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;
|
||
const alertBgColor = 'rgba(59, 130, 246, 0.1)';
|
||
const alertBorderColor = PROFESSIONAL_COLORS.border.default;
|
||
|
||
// 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);
|
||
|
||
// 通知横幅显示状态
|
||
const [showNotificationBanner, setShowNotificationBanner] = useState(false);
|
||
|
||
// 🎯 初始化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]);
|
||
|
||
// 🎯 追踪新闻列表查看(当事件列表加载完成后)
|
||
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]);
|
||
|
||
// ⚡ 检查通知权限状态,显示横幅提示
|
||
useEffect(() => {
|
||
// 延迟3秒显示,让用户先浏览页面
|
||
const timer = setTimeout(() => {
|
||
// 如果未授权或未请求过权限,显示横幅
|
||
if (browserPermission !== 'granted') {
|
||
const hasClosedBanner = localStorage.getItem('notification_banner_closed');
|
||
if (!hasClosedBanner) {
|
||
setShowNotificationBanner(true);
|
||
logger.info('Community', '显示通知权限横幅');
|
||
}
|
||
}
|
||
}, 3000);
|
||
|
||
return () => clearTimeout(timer);
|
||
}, [browserPermission]);
|
||
|
||
// 处理开启通知
|
||
const handleEnableNotifications = async () => {
|
||
const permission = await requestBrowserPermission();
|
||
if (permission === 'granted') {
|
||
setShowNotificationBanner(false);
|
||
logger.info('Community', '通知权限已授予');
|
||
}
|
||
};
|
||
|
||
// 处理关闭横幅
|
||
const handleCloseBanner = () => {
|
||
setShowNotificationBanner(false);
|
||
localStorage.setItem('notification_banner_closed', 'true');
|
||
logger.info('Community', '通知横幅已关闭');
|
||
};
|
||
|
||
// ⚡ 首次进入页面时滚动到内容区域(考虑导航栏高度)
|
||
const hasScrolled = useRef(false);
|
||
useEffect(() => {
|
||
// 只在第一次挂载时执行滚动
|
||
if (hasScrolled.current) return;
|
||
|
||
// 延迟执行,确保DOM已完全渲染
|
||
const timer = setTimeout(() => {
|
||
if (containerRef.current) {
|
||
hasScrolled.current = true;
|
||
// 滚动到容器顶部,自动考虑导航栏的高度
|
||
containerRef.current.scrollIntoView({
|
||
behavior: 'auto',
|
||
block: 'start',
|
||
inline: 'nearest'
|
||
});
|
||
}
|
||
}, 100);
|
||
|
||
return () => clearTimeout(timer);
|
||
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||
|
||
/**
|
||
* ⚡ 【核心逻辑】注册 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 }}>
|
||
{/* 通知权限提示横幅 */}
|
||
{showNotificationBanner && (
|
||
<Alert
|
||
status="info"
|
||
variant="subtle"
|
||
borderRadius="lg"
|
||
mb={4}
|
||
boxShadow="md"
|
||
bg={alertBgColor}
|
||
borderWidth="1px"
|
||
borderColor={alertBorderColor}
|
||
>
|
||
<AlertIcon />
|
||
<Box flex="1">
|
||
<AlertTitle fontSize="md" mb={1}>
|
||
开启桌面通知,不错过重要事件
|
||
</AlertTitle>
|
||
<AlertDescription fontSize="sm">
|
||
即使浏览器最小化,也能第一时间接收新事件推送通知
|
||
</AlertDescription>
|
||
</Box>
|
||
<HStack spacing={2} ml={4}>
|
||
<Button
|
||
size="sm"
|
||
colorScheme="blue"
|
||
onClick={handleEnableNotifications}
|
||
>
|
||
立即开启
|
||
</Button>
|
||
<CloseButton
|
||
onClick={handleCloseBanner}
|
||
position="relative"
|
||
/>
|
||
</HStack>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* 顶部说明面板:产品介绍 + 沪深指数 + 热门概念词云 */}
|
||
<HeroPanel />
|
||
|
||
{/* 实时要闻·动态追踪 - 横向滚动 */}
|
||
<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; |