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>
This commit is contained in:
zdl
2025-11-03 12:38:25 +08:00
parent 39a2ccd53b
commit cc2777ae20
6 changed files with 304 additions and 15 deletions

View File

@@ -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 (
<Box position="relative">
{/* 左侧滚动按钮 */}
<Box>
{/* 分页控制器 - 右上角 */}
{totalPages > 1 && (
<Flex justify="flex-end" mb={3}>
<PaginationControl
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
</Flex>
)}
{/* 横向滚动区域 */}
<Box position="relative">
{/* 左侧滚动按钮 */}
{showLeftArrow && (
<IconButton
icon={<ChevronLeftIcon boxSize={6} />}
@@ -168,6 +198,7 @@ const EventScrollList = ({
</Box>
))}
</Flex>
</Box>
</Box>
);
};

View File

@@ -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 (
<Box mb={3}>
<HStack spacing={1.5} justify="center" flexWrap="wrap">
{/* 上一页按钮 */}
<IconButton
icon={<ChevronLeftIcon />}
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 (
<Text
key={`ellipsis-${index}`}
px={1}
fontSize="xs"
color="gray.500"
>
...
</Text>
);
}
return (
<Button
key={page}
size="xs"
onClick={() => onPageChange(page)}
bg={currentPage === page ? activeBg : buttonBg}
color={currentPage === page ? activeColor : undefined}
borderWidth="1px"
borderColor={currentPage === page ? activeBg : borderColor}
_hover={{
bg: currentPage === page ? activeBg : hoverBg,
}}
minW="28px"
>
{page}
</Button>
);
})}
{/* 下一页按钮 */}
<IconButton
icon={<ChevronRightIcon />}
size="xs"
onClick={() => onPageChange(currentPage + 1)}
isDisabled={currentPage === totalPages}
bg={buttonBg}
borderWidth="1px"
borderColor={borderColor}
_hover={{ bg: hoverBg }}
aria-label="下一页"
title="下一页"
/>
{/* 分隔线 */}
<Box w="1px" h="20px" bg={borderColor} mx={1.5} />
{/* 输入框跳转 */}
<HStack spacing={1.5}>
<Text fontSize="xs" color="gray.600">
跳转到
</Text>
<Input
size="xs"
width="50px"
type="number"
min={1}
max={totalPages}
value={jumpPage}
onChange={(e) => setJumpPage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="页"
bg={buttonBg}
borderColor={borderColor}
/>
<Button
size="xs"
colorScheme="blue"
onClick={handleJump}
>
跳转
</Button>
</HStack>
</HStack>
</Box>
);
};
export default PaginationControl;