feat: 优化社区动态新闻分页和预加载策略

## 主要改动

### 1. 修复分页显示问题
- 修复总页数计算错误(使用服务端 total 而非缓存 cachedCount)
- 修复目标页数据检查逻辑(排除 null 占位符)

### 2. 实现请求拆分策略 (Critical Fix)
- 将合并请求(per_page: 15)拆分为单页循环请求(per_page: 5)
- 解决后端无法处理动态 per_page 导致返回空数据的问题
- 后台预加载和显示 loading 两个场景均已拆分

### 3. 优化智能预加载逻辑
- 连续翻页(上/下页):预加载前后各 2 页
- 跳转翻页(点页码):只加载当前页
- 目标页已缓存时立即切换,后台静默预加载其他页

### 4. Redux 状态管理优化
- 添加 pageSize 参数用于正确计算索引
- 重写 reducer 插入逻辑(append/replace/jump 三种模式)
- 只在 append 模式去重,避免替换和跳页时数据丢失
- 修复 selector 计算有效数量(排除 null)

### 5. 修复 React Hook 规则违规
- 将所有 useColorModeValue 移至组件顶层
- 添加缺失的 HStack 导入

## 影响范围
- 仅影响社区页面动态新闻分页功能
- 无后端变更,向后兼容

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-04 11:43:44 +08:00
parent 012c13c49a
commit ce46820105
3 changed files with 344 additions and 65 deletions

View File

@@ -11,6 +11,7 @@ import {
ButtonGroup,
Center,
VStack,
HStack,
Spinner,
Text,
useColorModeValue
@@ -28,7 +29,8 @@ import PaginationControl from './PaginationControl';
* @param {number} currentPage - 当前页码
* @param {number} totalPages - 总页数(由服务端返回)
* @param {Function} onPageChange - 页码改变回调
* @param {boolean} loading - 加载状态
* @param {boolean} loading - 全局加载状态
* @param {number|null} loadingPage - 正在加载的目标页码(用于显示"正在加载第X页..."
* @param {string} mode - 展示模式:'carousel'(单排轮播)| 'grid'(双排网格)
* @param {Function} onModeChange - 模式切换回调
* @param {boolean} hasMore - 是否还有更多数据
@@ -52,20 +54,37 @@ const EventScrollList = ({
}) => {
const scrollContainerRef = useRef(null);
// 时间轴样式配置
// 所有 useColorModeValue 必须在组件顶层调用(不能在条件渲染中)
const timelineBg = useColorModeValue('gray.50', 'gray.700');
const timelineBorderColor = useColorModeValue('gray.400', 'gray.500');
const timelineTextColor = useColorModeValue('blue.600', 'blue.400');
// 翻页按钮颜色
const arrowBtnBg = useColorModeValue('rgba(255, 255, 255, 0.9)', 'rgba(0, 0, 0, 0.6)');
const arrowBtnHoverBg = useColorModeValue('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0.8)');
// 滚动条颜色
const scrollbarTrackBg = useColorModeValue('#f1f1f1', '#2D3748');
const scrollbarThumbBg = useColorModeValue('#888', '#4A5568');
const scrollbarThumbHoverBg = useColorModeValue('#555', '#718096');
// 加载遮罩颜色
const loadingOverlayBg = useColorModeValue('whiteAlpha.800', 'blackAlpha.700');
const loadingTextColor = useColorModeValue('gray.600', 'gray.300');
const getTimelineBoxStyle = () => {
return {
bg: useColorModeValue('gray.50', 'gray.700'),
borderColor: useColorModeValue('gray.400', 'gray.500'),
bg: timelineBg,
borderColor: timelineBorderColor,
borderWidth: '2px',
textColor: useColorModeValue('blue.600', 'blue.400'),
textColor: timelineTextColor,
boxShadow: 'sm',
};
};
return (
<Box>
{/* 顶部控制栏:模式切换按钮(左)+ 分页控制器(右) */}
{/* 顶部控制栏:模式切换按钮(左)+ 分页控制器 + 加载提示(右) */}
<Flex justify="space-between" align="center" mb={2}>
{/* 模式切换按钮 */}
<ButtonGroup size="sm" isAttached>
@@ -87,11 +106,11 @@ const EventScrollList = ({
{/* 分页控制器 */}
{totalPages > 1 && (
<PaginationControl
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
<PaginationControl
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
)}
</Flex>
@@ -114,10 +133,10 @@ const EventScrollList = ({
h="40px"
minW="40px"
borderRadius="full"
bg={useColorModeValue('rgba(255, 255, 255, 0.9)', 'rgba(0, 0, 0, 0.6)')}
bg={arrowBtnBg}
boxShadow="0 2px 8px rgba(0, 0, 0, 0.15)"
_hover={{
bg: useColorModeValue('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0.8)'),
bg: arrowBtnHoverBg,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
transform: 'translateY(-50%) scale(1.05)'
}}
@@ -142,10 +161,10 @@ const EventScrollList = ({
h="40px"
minW="40px"
borderRadius="full"
bg={useColorModeValue('rgba(255, 255, 255, 0.9)', 'rgba(0, 0, 0, 0.6)')}
bg={arrowBtnBg}
boxShadow="0 2px 8px rgba(0, 0, 0, 0.15)"
_hover={{
bg: useColorModeValue('rgba(255, 255, 255, 1)', 'rgba(0, 0, 0, 0.8)'),
bg: arrowBtnHoverBg,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
transform: 'translateY(-50%) scale(1.05)'
}}
@@ -169,15 +188,15 @@ const EventScrollList = ({
height: '8px',
},
'&::-webkit-scrollbar-track': {
background: useColorModeValue('#f1f1f1', '#2D3748'),
background: scrollbarTrackBg,
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb': {
background: useColorModeValue('#888', '#4A5568'),
background: scrollbarThumbBg,
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: useColorModeValue('#555', '#718096'),
background: scrollbarThumbHoverBg,
},
scrollBehavior: 'smooth',
WebkitOverflowScrolling: 'touch',
@@ -191,14 +210,14 @@ const EventScrollList = ({
left={0}
right={0}
bottom={0}
bg={useColorModeValue('whiteAlpha.800', 'blackAlpha.700')}
bg={loadingOverlayBg}
backdropFilter="blur(2px)"
zIndex={10}
borderRadius="md"
>
<VStack>
<Spinner size="lg" color="blue.500" thickness="3px" />
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.300')}>
<Text fontSize="sm" color={loadingTextColor}>
加载中...
</Text>
</VStack>