Files
vf_react/src/views/Community/index.js
zdl 3b5caf8a01 feat: 实现实时要闻服务端分页功能
功能新增:
- 实时要闻组件支持服务端分页,每次切换页码重新请求数据
- 分页控制器组件,支持数字页码、上下翻页、快速跳转
- Mock 数据量从 100 条增加到 200 条,支持分页测试

技术实现:

1. Redux 状态管理(communityDataSlice.js)
   - fetchDynamicNews 接收分页参数 { page, per_page }
   - 返回数据结构调整为 { events, pagination }
   - initialState 新增 dynamicNewsPagination 字段
   - Reducer 分别存储 events 和 pagination 信息
   - Selector 返回完整的 pagination 数据

2. 组件层(index.js → DynamicNewsCard → EventScrollList)
   - Community/index.js: 获取并传递 pagination 信息
   - DynamicNewsCard.js: 管理分页状态,触发服务端请求
   - EventScrollList.js: 接收服务端 totalPages,渲染当前页数据
   - 页码切换时自动选中第一个事件

3. 分页控制器(PaginationControl.js)
   - 精简版设计:移除首页/末页按钮
   - 上一页/下一页按钮,边界状态自动禁用
   - 智能页码列表(最多5个,使用省略号)
   - 输入框跳转功能,支持回车键
   - Toast 提示非法输入
   - 全部使用 xs 尺寸,紧凑布局

4. Mock 数据(events.js)
   - 总事件数从 100 增加到 200 条
   - 支持服务端分页测试(40 页 × 5 条/页)

分页流程:
1. 初始加载:请求 page=1, per_page=5
2. 切换页码:dispatch(fetchDynamicNews({ page: 2, per_page: 5 }))
3. 后端返回:{ events: [5条], pagination: { page, total, total_pages } }
4. 前端更新:显示新页面数据,更新分页控制器状态

UI 优化:
- 紧凑的分页控制器布局
- 移除冗余元素(首页/末页/总页数提示)
- xs 尺寸按钮,减少视觉负担
- 保留核心功能(翻页、页码、跳转)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 12:38:25 +08:00

226 lines
7.4 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, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import {
fetchPopularKeywords,
fetchHotEvents,
fetchDynamicNews,
selectDynamicNewsWithLoading
} from '../../store/slices/communityDataSlice';
import {
Box,
Container,
useColorModeValue,
} from '@chakra-ui/react';
// 导入组件
import EventTimelineCard from './components/EventTimelineCard';
import DynamicNewsCard from './components/DynamicNewsCard';
import MarketReviewCard from './components/MarketReviewCard';
import HotEventsSection from './components/HotEventsSection';
import EventModals from './components/EventModals';
// 导入自定义 Hooks
import { useEventData } from './hooks/useEventData';
import { useEventFilters } from './hooks/useEventFilters';
import { useCommunityEvents } from './hooks/useCommunityEvents';
// 导入时间工具函数
import {
getCurrentTradingTimeRange,
getMarketReviewTimeRange,
filterEventsByTimeRange
} from '../../utils/tradingTimeUtils';
import { logger } from '../../utils/logger';
import { useNotification } from '../../contexts/NotificationContext';
import { usePostHogTrack } from '../../hooks/usePostHogRedux';
import { RETENTION_EVENTS } from '../../lib/constants';
// 导航栏已由 MainLayout 提供,无需在此导入
const Community = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { track } = usePostHogTrack(); // PostHog 追踪(保留用于兼容)
// Redux状态
const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
const {
data: dynamicNewsEvents,
loading: dynamicNewsLoading,
error: dynamicNewsError,
pagination: dynamicNewsPagination
} = useSelector(selectDynamicNewsWithLoading);
// Chakra UI hooks
const bgColor = useColorModeValue('gray.50', 'gray.900');
// Ref用于滚动到实时事件时间轴
const eventTimelineRef = useRef(null);
const hasScrolledRef = useRef(false); // 标记是否已滚动
// ⚡ 通知权限引导
const { showCommunityGuide } = useNotification();
// Modal/Drawer状态
const [selectedEvent, setSelectedEvent] = useState(null);
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
// 🎯 初始化Community埋点Hook
const communityEvents = useCommunityEvents({ navigate });
// 自定义 Hooks
const { filters, updateFilters, handlePageChange, handleEventClick, handleViewDetail } = useEventFilters({
navigate,
onEventClick: (event) => setSelectedEventForStock(event),
eventTimelineRef
});
const { events, pagination, loading, lastUpdateTime } = useEventData(filters);
// 计算市场复盘的时间范围和过滤后的事件
const marketReviewData = useMemo(() => {
const timeRange = getMarketReviewTimeRange();
const filteredEvents = filterEventsByTimeRange(events, timeRange.startTime, timeRange.endTime);
logger.debug('Community', '市场复盘时间范围', {
description: timeRange.description,
rangeType: timeRange.rangeType,
eventCount: filteredEvents.length
});
return {
events: filteredEvents,
timeRange
};
}, [events]);
// 加载热门关键词、热点事件和动态新闻使用Redux
useEffect(() => {
dispatch(fetchPopularKeywords());
dispatch(fetchHotEvents());
dispatch(fetchDynamicNews());
}, [dispatch]);
// 每5分钟刷新一次动态新闻
useEffect(() => {
const interval = setInterval(() => {
dispatch(fetchDynamicNews());
}, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [dispatch]);
// 🎯 PostHog 追踪:页面浏览
// useEffect(() => {
// track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, {
// timestamp: new Date().toISOString(),
// has_hot_events: hotEvents && hotEvents.length > 0,
// has_keywords: popularKeywords && popularKeywords.length > 0,
// });
// }, [track]); // 只在组件挂载时执行一次
// 🎯 追踪新闻列表查看(当事件列表加载完成后)
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(() => {
if (showCommunityGuide) {
const timer = setTimeout(() => {
logger.info('Community', '显示社区权限引导');
showCommunityGuide();
}, 5000); // 延迟 5 秒,让用户先浏览页面
return () => clearTimeout(timer);
}
}, [showCommunityGuide]); // 只在组件挂载时执行一次
// ⚡ 滚动到实时事件区域(由搜索框聚焦触发)
const scrollToTimeline = useCallback(() => {
if (!hasScrolledRef.current && eventTimelineRef.current) {
eventTimelineRef.current.scrollIntoView({
behavior: 'smooth', // 平滑滚动动画
block: 'start', // 元素顶部对齐视口顶部,标题正好可见
inline: 'nearest' // 水平方向最小滚动
});
hasScrolledRef.current = true; // 标记已滚动
logger.debug('Community', '用户触发搜索,滚动到实时事件时间轴');
}
}, []);
return (
<Box minH="100vh" bg={bgColor}>
{/* 主内容区域 */}
<Container maxW="container.xl" pt={6} pb={8}>
{/* 热点事件区域 */}
<HotEventsSection events={hotEvents} />
{/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard
mt={6}
events={dynamicNewsEvents}
loading={dynamicNewsLoading}
pagination={dynamicNewsPagination}
lastUpdateTime={lastUpdateTime}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
/>
{/* 市场复盘 - 左右布局 */}
{/* <MarketReviewCard
mt={6}
events={marketReviewData.events}
loading={loading}
lastUpdateTime={lastUpdateTime}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
onToggleFollow={() => {}}
/> */}
{/* 实时事件 - 原纵向列表 */}
{/* <EventTimelineCard
ref={eventTimelineRef}
mt={6}
events={events}
loading={loading}
pagination={pagination}
filters={filters}
popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime}
onSearch={updateFilters}
onSearchFocus={scrollToTimeline}
onPageChange={handlePageChange}
onEventClick={handleEventClick}
onViewDetail={handleViewDetail}
/> */}
</Container>
{/* 事件弹窗 */}
<EventModals
eventModalState={{
isOpen: !!selectedEvent,
onClose: () => setSelectedEvent(null),
event: selectedEvent,
onEventClose: () => setSelectedEvent(null)
}}
stockDrawerState={{
visible: !!selectedEventForStock,
event: selectedEventForStock,
onClose: () => setSelectedEventForStock(null)
}}
/>
</Box>
);
};
export default Community;