diff --git a/src/mocks/data/events.js b/src/mocks/data/events.js index cc6d2d0d..021d57f1 100644 --- a/src/mocks/data/events.js +++ b/src/mocks/data/events.js @@ -726,8 +726,8 @@ export function generateMockEvents(params = {}) { stock_code = '', } = params; - // 生成100个事件用于测试 - const totalEvents = 100; + // 生成200个事件用于测试(足够测试分页功能) + const totalEvents = 200; const allEvents = []; const importanceLevels = ['S', 'A', 'B', 'C']; diff --git a/src/store/slices/communityDataSlice.js b/src/store/slices/communityDataSlice.js index b8f8524b..d05f3aaa 100644 --- a/src/store/slices/communityDataSlice.js +++ b/src/store/slices/communityDataSlice.js @@ -159,28 +159,33 @@ export const fetchHotEvents = createAsyncThunk( /** * 获取动态新闻(无缓存,每次都发起请求) * 用于 DynamicNewsCard 组件,需要保持实时性 + * @param {Object} params - 分页参数 { page, per_page } */ export const fetchDynamicNews = createAsyncThunk( 'communityData/fetchDynamicNews', - async (_, { rejectWithValue }) => { + async ({ page = 1, per_page = 5 } = {}, { rejectWithValue }) => { try { - logger.debug('CommunityData', '开始获取动态新闻'); + logger.debug('CommunityData', '开始获取动态新闻', { page, per_page }); const response = await eventService.getEvents({ - page: 1, - per_page: 5, + page, + per_page, sort: 'new' }); if (response.success && response.data?.events) { logger.info('CommunityData', '动态新闻加载成功', { count: response.data.events.length, + page: response.data.pagination?.page || page, total: response.data.pagination?.total || 0 }); - return response.data.events; + return { + events: response.data.events, + pagination: response.data.pagination || {} + }; } logger.warn('CommunityData', '动态新闻返回数据为空', response); - return []; + return { events: [], pagination: {} }; } catch (error) { logger.error('CommunityData', '获取动态新闻失败', error); return rejectWithValue(error.message || '获取动态新闻失败'); @@ -197,6 +202,7 @@ const communityDataSlice = createSlice({ popularKeywords: [], hotEvents: [], dynamicNews: [], // 动态新闻(无缓存) + dynamicNewsPagination: {}, // 动态新闻分页信息 // 加载状态 loading: { @@ -279,7 +285,24 @@ const communityDataSlice = createSlice({ // 使用工厂函数创建 reducers,消除重复代码 createDataReducers(builder, fetchPopularKeywords, 'popularKeywords'); createDataReducers(builder, fetchHotEvents, 'hotEvents'); - createDataReducers(builder, fetchDynamicNews, 'dynamicNews'); + + // dynamicNews 需要特殊处理(包含 pagination) + builder + .addCase(fetchDynamicNews.pending, (state) => { + state.loading.dynamicNews = true; + state.error.dynamicNews = null; + }) + .addCase(fetchDynamicNews.fulfilled, (state, action) => { + state.loading.dynamicNews = false; + state.dynamicNews = action.payload.events; + state.dynamicNewsPagination = action.payload.pagination; + state.lastUpdated.dynamicNews = new Date().toISOString(); + }) + .addCase(fetchDynamicNews.rejected, (state, action) => { + state.loading.dynamicNews = false; + state.error.dynamicNews = action.payload; + logger.error('CommunityData', 'dynamicNews 加载失败', new Error(action.payload)); + }); } }); @@ -314,6 +337,7 @@ export const selectDynamicNewsWithLoading = (state) => ({ data: state.communityData.dynamicNews, loading: state.communityData.loading.dynamicNews, error: state.communityData.error.dynamicNews, + pagination: state.communityData.dynamicNewsPagination, lastUpdated: state.communityData.lastUpdated.dynamicNews }); diff --git a/src/views/Community/components/DynamicNewsCard.js b/src/views/Community/components/DynamicNewsCard.js index e273d10a..2e6b8fdd 100644 --- a/src/views/Community/components/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNewsCard.js @@ -2,6 +2,7 @@ // 横向滚动事件卡片组件(实时要闻·动态追踪) import React, { forwardRef, useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; import { Card, CardHeader, @@ -20,11 +21,13 @@ import { import { TimeIcon } from '@chakra-ui/icons'; import EventScrollList from './DynamicNewsCard/EventScrollList'; import DynamicNewsDetailPanel from './DynamicNewsDetail'; +import { fetchDynamicNews } from '../../../store/slices/communityDataSlice'; /** * 实时要闻·动态追踪 - 横向滚动卡片组件 * @param {Array} events - 事件列表 * @param {boolean} loading - 加载状态 + * @param {Object} pagination - 分页信息 { page, per_page, total, total_pages } * @param {Date} lastUpdateTime - 最后更新时间 * @param {Function} onEventClick - 事件点击回调 * @param {Function} onViewDetail - 查看详情回调 @@ -33,15 +36,21 @@ import DynamicNewsDetailPanel from './DynamicNewsDetail'; const DynamicNewsCard = forwardRef(({ events, loading, + pagination = {}, lastUpdateTime, onEventClick, onViewDetail, ...rest }, ref) => { + const dispatch = useDispatch(); const cardBg = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.700'); const [selectedEvent, setSelectedEvent] = useState(null); + const pageSize = 5; // 每页显示5个事件 + const currentPage = pagination.page || 1; + const totalPages = pagination.total_pages || 1; + // 默认选中第一个事件 useEffect(() => { if (events && events.length > 0 && !selectedEvent) { @@ -49,6 +58,15 @@ const DynamicNewsCard = forwardRef(({ } }, [events, selectedEvent]); + // 页码改变时,触发服务端分页请求 + const handlePageChange = (newPage) => { + // 发起 Redux action 获取新页面数据 + dispatch(fetchDynamicNews({ page: newPage, per_page: pageSize })); + + // 重置选中事件(等新数据加载后自动选中第一个) + setSelectedEvent(null); + }; + return ( {/* 标题部分 */} @@ -101,6 +119,9 @@ const DynamicNewsCard = forwardRef(({ selectedEvent={selectedEvent} onEventSelect={setSelectedEvent} borderColor={borderColor} + currentPage={currentPage} + totalPages={totalPages} + onPageChange={handlePageChange} /> )} diff --git a/src/views/Community/components/DynamicNewsCard/EventScrollList.js b/src/views/Community/components/DynamicNewsCard/EventScrollList.js index 75ce1959..78dafbc7 100644 --- a/src/views/Community/components/DynamicNewsCard/EventScrollList.js +++ b/src/views/Community/components/DynamicNewsCard/EventScrollList.js @@ -1,7 +1,7 @@ // src/views/Community/components/DynamicNewsCard/EventScrollList.js // 横向滚动事件列表组件 -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { Box, Flex, @@ -10,24 +10,41 @@ import { } from '@chakra-ui/react'; import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; import DynamicNewsEventCard from '../EventCard/DynamicNewsEventCard'; +import PaginationControl from './PaginationControl'; /** * 横向滚动事件列表组件 - * @param {Array} events - 事件列表 + * @param {Array} events - 当前页的事件列表(服务端已分页) * @param {Object} selectedEvent - 当前选中的事件 * @param {Function} onEventSelect - 事件选择回调 * @param {string} borderColor - 边框颜色 + * @param {number} currentPage - 当前页码 + * @param {number} totalPages - 总页数(由服务端返回) + * @param {Function} onPageChange - 页码改变回调 */ const EventScrollList = ({ events, selectedEvent, onEventSelect, - borderColor + borderColor, + currentPage, + totalPages, + onPageChange }) => { const scrollContainerRef = useRef(null); const [showLeftArrow, setShowLeftArrow] = useState(false); const [showRightArrow, setShowRightArrow] = useState(true); + // 页码变化时,滚动到左侧起始位置 + useEffect(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + left: 0, + behavior: 'smooth' + }); + } + }, [currentPage]); + // 滚动到左侧 const scrollLeft = () => { if (scrollContainerRef.current) { @@ -71,8 +88,21 @@ const EventScrollList = ({ }; return ( - - {/* 左侧滚动按钮 */} + + {/* 分页控制器 - 右上角 */} + {totalPages > 1 && ( + + + + )} + + {/* 横向滚动区域 */} + + {/* 左侧滚动按钮 */} {showLeftArrow && ( } @@ -168,6 +198,7 @@ const EventScrollList = ({ ))} + ); }; diff --git a/src/views/Community/components/DynamicNewsCard/PaginationControl.js b/src/views/Community/components/DynamicNewsCard/PaginationControl.js new file mode 100644 index 00000000..5b619029 --- /dev/null +++ b/src/views/Community/components/DynamicNewsCard/PaginationControl.js @@ -0,0 +1,211 @@ +// src/views/Community/components/DynamicNewsCard/PaginationControl.js +// 分页控制器组件 + +import React, { useState } from 'react'; +import { + Box, + HStack, + Button, + Input, + Text, + IconButton, + useColorModeValue, + useToast, +} from '@chakra-ui/react'; +import { + ChevronLeftIcon, + ChevronRightIcon, +} from '@chakra-ui/icons'; + +/** + * 分页控制器组件 + * @param {number} currentPage - 当前页码 + * @param {number} totalPages - 总页数 + * @param {Function} onPageChange - 页码改变回调 + */ +const PaginationControl = ({ currentPage, totalPages, onPageChange }) => { + const [jumpPage, setJumpPage] = useState(''); + const toast = useToast(); + + const buttonBg = useColorModeValue('white', 'gray.700'); + const activeBg = useColorModeValue('blue.500', 'blue.400'); + const activeColor = useColorModeValue('white', 'white'); + const borderColor = useColorModeValue('gray.300', 'gray.600'); + const hoverBg = useColorModeValue('gray.100', 'gray.600'); + + // 生成页码数字列表(智能省略) + const getPageNumbers = () => { + const pageNumbers = []; + const maxVisible = 5; // 最多显示5个页码(精简版) + + if (totalPages <= maxVisible) { + // 总页数少,显示全部 + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // 总页数多,使用省略号 + if (currentPage <= 3) { + // 当前页在前面 + for (let i = 1; i <= 4; i++) { + pageNumbers.push(i); + } + pageNumbers.push('...'); + pageNumbers.push(totalPages); + } else if (currentPage >= totalPages - 2) { + // 当前页在后面 + pageNumbers.push(1); + pageNumbers.push('...'); + for (let i = totalPages - 3; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // 当前页在中间 + pageNumbers.push(1); + pageNumbers.push('...'); + pageNumbers.push(currentPage); + pageNumbers.push('...'); + pageNumbers.push(totalPages); + } + } + + return pageNumbers; + }; + + // 处理页码跳转 + const handleJump = () => { + const page = parseInt(jumpPage, 10); + + if (isNaN(page)) { + toast({ + title: '请输入有效的页码', + status: 'warning', + duration: 2000, + isClosable: true, + }); + return; + } + + if (page < 1 || page > totalPages) { + toast({ + title: `页码范围:1 - ${totalPages}`, + status: 'warning', + duration: 2000, + isClosable: true, + }); + return; + } + + onPageChange(page); + setJumpPage(''); + }; + + // 处理回车键 + const handleKeyPress = (e) => { + if (e.key === 'Enter') { + handleJump(); + } + }; + + const pageNumbers = getPageNumbers(); + + return ( + + + {/* 上一页按钮 */} + } + size="xs" + onClick={() => onPageChange(currentPage - 1)} + isDisabled={currentPage === 1} + bg={buttonBg} + borderWidth="1px" + borderColor={borderColor} + _hover={{ bg: hoverBg }} + aria-label="上一页" + title="上一页" + /> + + {/* 数字页码列表 */} + {pageNumbers.map((page, index) => { + if (page === '...') { + return ( + + ... + + ); + } + + return ( + + ); + })} + + {/* 下一页按钮 */} + } + size="xs" + onClick={() => onPageChange(currentPage + 1)} + isDisabled={currentPage === totalPages} + bg={buttonBg} + borderWidth="1px" + borderColor={borderColor} + _hover={{ bg: hoverBg }} + aria-label="下一页" + title="下一页" + /> + + {/* 分隔线 */} + + + {/* 输入框跳转 */} + + + 跳转到 + + setJumpPage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="页" + bg={buttonBg} + borderColor={borderColor} + /> + + + + + ); +}; + +export default PaginationControl; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 1f92c805..9ff47375 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -50,7 +50,8 @@ const Community = () => { const { data: dynamicNewsEvents, loading: dynamicNewsLoading, - error: dynamicNewsError + error: dynamicNewsError, + pagination: dynamicNewsPagination } = useSelector(selectDynamicNewsWithLoading); // Chakra UI hooks @@ -169,6 +170,7 @@ const Community = () => { mt={6} events={dynamicNewsEvents} loading={dynamicNewsLoading} + pagination={dynamicNewsPagination} lastUpdateTime={lastUpdateTime} onEventClick={handleEventClick} onViewDetail={handleViewDetail}