功能新增:
- 实时要闻组件支持服务端分页,每次切换页码重新请求数据
- 分页控制器组件,支持数字页码、上下翻页、快速跳转
- 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>
226 lines
7.4 KiB
JavaScript
226 lines
7.4 KiB
JavaScript
// 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; |