Files
vf_react/src/views/Community/index.js
zdl e4d9bc4124 feat: 实现 Socket 触发的智能列表自动刷新功能(带防抖)
核心改动:
- 扩展 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>
2025-11-14 19:04:00 +08:00

297 lines
10 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, 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;