refactor: Community 目录结构重组 + 修复导入路径 + 添加 Mock 数据
## 目录重构 - DynamicNewsCard/ → DynamicNews/(含 layouts/, hooks/ 子目录) - EventCard 原子组件 → EventCard/atoms/ - EventDetailModal 独立目录化 - HotEvents 独立目录化(含 CSS) - SearchFilters 独立目录化(CompactSearchBox, TradingTimeFilter) ## 导入路径修复 - EventCard/*.js: 统一使用 @constants/, @utils/, @components/ 别名 - atoms/*.js: 修复移动后的相对路径问题 - DynamicNewsCard.js: 更新 contexts, store, constants 导入 - EventHeaderInfo.js, CompactMetaBar.js: 修复 EventFollowButton 导入 ## Mock Handler 添加 - /api/events/:eventId/expectation-score - 事件超预期得分 - /api/index/:indexCode/realtime - 指数实时行情 ## 警告修复 - CitationMark.js: overlayInnerStyle → styles (Antd 5.x) - CitedContent.js: 移除不支持的 jsx 属性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
// src/views/Community/components/DynamicNews/hooks/usePagination.js
|
||||
// 分页逻辑自定义 Hook
|
||||
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { fetchDynamicNews, updatePaginationPage } from '@store/slices/communityDataSlice';
|
||||
import { logger } from '@utils/logger';
|
||||
import {
|
||||
PAGINATION_CONFIG,
|
||||
DISPLAY_MODES,
|
||||
DEFAULT_MODE,
|
||||
TOAST_CONFIG
|
||||
} from '../constants';
|
||||
|
||||
/**
|
||||
* 分页逻辑自定义 Hook
|
||||
* @param {Object} options - Hook 配置选项
|
||||
* @param {Object} options.allCachedEventsByPage - 纵向模式页码映射 { 1: [...], 2: [...] }
|
||||
* @param {Array} options.allCachedEvents - 平铺模式数组 [...]
|
||||
* @param {Object} options.pagination - 分页元数据 { total, total_pages, current_page, per_page, page }
|
||||
* @param {number} options.total - 【废弃】服务端总数量(向后兼容,建议使用 pagination.total)
|
||||
* @param {number} options.cachedCount - 已缓存数量
|
||||
* @param {Function} options.dispatch - Redux dispatch 函数
|
||||
* @param {Function} options.toast - Toast 通知函数
|
||||
* @param {Object} options.filters - 筛选条件
|
||||
* @param {string} options.initialMode - 初始显示模式(可选)
|
||||
* @returns {Object} 分页状态和方法
|
||||
*/
|
||||
export const usePagination = ({
|
||||
allCachedEventsByPage, // 纵向模式:页码映射
|
||||
allCachedEvents, // 平铺模式:数组
|
||||
pagination, // 分页元数据对象
|
||||
total, // 向后兼容
|
||||
cachedCount,
|
||||
dispatch,
|
||||
toast,
|
||||
filters = {},
|
||||
initialMode // 初始显示模式
|
||||
}) => {
|
||||
// 本地状态
|
||||
const [loadingPage, setLoadingPage] = useState(null);
|
||||
const [mode, setMode] = useState(initialMode || DEFAULT_MODE);
|
||||
|
||||
// 【核心改动】从 Redux pagination 派生 currentPage,不再使用本地状态
|
||||
const currentPage = pagination?.current_page || PAGINATION_CONFIG.INITIAL_PAGE;
|
||||
|
||||
// 使用 ref 存储最新的 filters,避免 useCallback 闭包问题
|
||||
// 当 filters 对象引用不变但内容改变时,闭包中的 filters 是旧值
|
||||
const filtersRef = useRef(filters);
|
||||
filtersRef.current = filters;
|
||||
|
||||
// 根据模式决定每页显示数量
|
||||
const pageSize = (() => {
|
||||
switch (mode) {
|
||||
case DISPLAY_MODES.FOUR_ROW:
|
||||
return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE;
|
||||
case DISPLAY_MODES.VERTICAL:
|
||||
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
|
||||
default:
|
||||
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
|
||||
}
|
||||
})();
|
||||
|
||||
// 【优化】优先使用后端返回的 total_pages,避免前端重复计算
|
||||
// 向后兼容:如果没有 pagination 对象,则使用 total 计算
|
||||
const totalPages = pagination?.total_pages || Math.ceil((pagination?.total || total || 0) / pageSize) || 1;
|
||||
|
||||
// 检查是否还有更多数据(使用页码判断,不受去重影响)
|
||||
const hasMore = currentPage < totalPages;
|
||||
|
||||
// 从页码映射或数组获取当前页数据
|
||||
const currentPageEvents = useMemo(() => {
|
||||
if (mode === DISPLAY_MODES.VERTICAL) {
|
||||
// 纵向模式:从页码映射获取当前页
|
||||
return allCachedEventsByPage?.[currentPage] || [];
|
||||
} else {
|
||||
// 平铺模式:返回全部累积数据
|
||||
return allCachedEvents || [];
|
||||
}
|
||||
}, [mode, allCachedEventsByPage, allCachedEvents, currentPage]);
|
||||
|
||||
// 当前显示的事件列表
|
||||
const displayEvents = useMemo(() => {
|
||||
if (mode === DISPLAY_MODES.FOUR_ROW) {
|
||||
// 平铺模式:返回全部累积数据
|
||||
return allCachedEvents || [];
|
||||
} else {
|
||||
// 纵向模式:返回当前页数据
|
||||
return currentPageEvents;
|
||||
}
|
||||
}, [mode, allCachedEvents, currentPageEvents]);
|
||||
|
||||
/**
|
||||
* 加载单个页面数据
|
||||
* @param {number} targetPage - 目标页码
|
||||
* @param {boolean} clearCache - 是否清空缓存(第1页专用)
|
||||
* @returns {Promise<boolean>} 是否加载成功
|
||||
*/
|
||||
const loadPage = useCallback(async (targetPage, clearCache = false) => {
|
||||
// 显示 loading 状态
|
||||
setLoadingPage(targetPage);
|
||||
|
||||
try {
|
||||
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
|
||||
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}, clearCache=${clearCache}`, 'color: #16A34A;');
|
||||
console.log(`%c 筛选条件:`, 'color: #16A34A;', filtersRef.current);
|
||||
|
||||
logger.debug('DynamicNewsCard', '开始加载页面数据', {
|
||||
targetPage,
|
||||
pageSize,
|
||||
mode,
|
||||
clearCache,
|
||||
filters: filtersRef.current
|
||||
});
|
||||
|
||||
// 🔍 调试:dispatch 前
|
||||
console.log(`%c🔵 [dispatch] 准备调用 fetchDynamicNews`, 'color: #3B82F6; font-weight: bold;', {
|
||||
mode,
|
||||
page: targetPage,
|
||||
per_page: pageSize,
|
||||
pageSize,
|
||||
clearCache,
|
||||
filters: filtersRef.current
|
||||
});
|
||||
|
||||
const result = await dispatch(fetchDynamicNews({
|
||||
mode: mode, // 传递 mode 参数
|
||||
per_page: pageSize,
|
||||
pageSize: pageSize,
|
||||
clearCache: clearCache, // 传递 clearCache 参数
|
||||
...filtersRef.current, // 从 ref 读取最新筛选条件
|
||||
page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖)
|
||||
})).unwrap();
|
||||
|
||||
// 🔍 调试:dispatch 后
|
||||
console.log(`%c🔵 [dispatch] fetchDynamicNews 返回结果`, 'color: #3B82F6; font-weight: bold;', result);
|
||||
console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;');
|
||||
logger.debug('DynamicNewsCard', `第 ${targetPage} 页加载完成`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DynamicNewsCard', 'loadPage', error, {
|
||||
targetPage
|
||||
});
|
||||
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: `无法加载第 ${targetPage} 页数据,请稍后重试`,
|
||||
status: 'error',
|
||||
duration: TOAST_CONFIG.DURATION_ERROR,
|
||||
isClosable: true,
|
||||
position: 'top'
|
||||
});
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setLoadingPage(null);
|
||||
}
|
||||
}, [dispatch, pageSize, toast, mode]); // 移除 filters 依赖,使用 filtersRef 读取最新值
|
||||
|
||||
// 翻页处理(第1页强制刷新 + 其他页缓存)
|
||||
const handlePageChange = useCallback(async (newPage, force = false) => {
|
||||
// force 参数:是否强制刷新(绕过"重复点击"检查)
|
||||
// - true: 强制刷新(Socket 新事件触发)
|
||||
// - false: 正常翻页(用户点击分页按钮)
|
||||
|
||||
// 边界检查 1: 检查页码范围
|
||||
if (newPage < 1 || newPage > totalPages) {
|
||||
console.log(`%c⚠️ [翻页] 页码超出范围: ${newPage}`, 'color: #DC2626; font-weight: bold;');
|
||||
logger.warn('usePagination', '页码超出范围', { newPage, totalPages });
|
||||
return;
|
||||
}
|
||||
|
||||
// 边界检查 2: 检查是否重复点击(强制刷新时绕过此检查)
|
||||
if (!force && newPage === currentPage) {
|
||||
console.log(`%c⚠️ [翻页] 重复点击当前页: ${newPage}`, 'color: #EAB308; font-weight: bold;');
|
||||
logger.debug('usePagination', '页码未改变', { newPage });
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚡ 如果是强制刷新(force = true),即使页码相同也继续执行
|
||||
if (force && newPage === currentPage) {
|
||||
console.log(`%c🔄 [翻页] 强制刷新当前页: ${newPage}`, 'color: #10B981; font-weight: bold;');
|
||||
logger.info('usePagination', '强制刷新当前页', { newPage });
|
||||
}
|
||||
|
||||
// 边界检查 3: 防止竞态条件 - 只拦截相同页面的重复请求
|
||||
if (loadingPage === newPage) {
|
||||
console.log(`%c⚠️ [翻页] 第${newPage}页正在加载中,忽略重复请求`, 'color: #EAB308; font-weight: bold;');
|
||||
logger.warn('usePagination', '竞态条件:相同页面正在加载', { loadingPage, newPage });
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果正在加载其他页面,允许切换(会取消当前加载状态,开始新的加载)
|
||||
if (loadingPage !== null && loadingPage !== newPage) {
|
||||
console.log(`%c🔄 [翻页] 正在加载第${loadingPage}页,用户切换到第${newPage}页`, 'color: #8B5CF6; font-weight: bold;');
|
||||
logger.info('usePagination', '用户切换页面,继续处理新请求', { loadingPage, newPage });
|
||||
// 继续执行,loadPage 会覆盖 loadingPage 状态
|
||||
}
|
||||
|
||||
console.log(`%c🔵 [翻页逻辑] handlePageChange 开始`, 'color: #3B82F6; font-weight: bold;');
|
||||
console.log(`%c 当前页: ${currentPage}, 目标页: ${newPage}, 模式: ${mode}`, 'color: #3B82F6;');
|
||||
|
||||
// 【核心逻辑】第1页特殊处理:强制清空缓存并重新加载
|
||||
if (newPage === 1) {
|
||||
console.log(`%c🔄 [第1页] 清空缓存并重新加载`, 'color: #8B5CF6; font-weight: bold;');
|
||||
logger.info('usePagination', '第1页:强制刷新', { mode });
|
||||
|
||||
// clearCache = true:API 会更新 Redux pagination.current_page
|
||||
await loadPage(newPage, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 【其他页】检查缓存
|
||||
if (mode === DISPLAY_MODES.VERTICAL) {
|
||||
// 纵向模式:检查页码映射中是否有缓存
|
||||
const isPageCached = allCachedEventsByPage?.[newPage]?.length > 0;
|
||||
|
||||
console.log(`%c🟡 [缓存检查] 第${newPage}页缓存状态`, 'color: #EAB308; font-weight: bold;');
|
||||
console.log(`%c 是否已缓存: ${isPageCached ? '✅ 是' : '❌ 否'}`, `color: ${isPageCached ? '#16A34A' : '#DC2626'};`);
|
||||
|
||||
if (isPageCached) {
|
||||
console.log(`%c✅ [缓存] 第${newPage}页已缓存,直接切换`, 'color: #16A34A; font-weight: bold;');
|
||||
// 使用缓存数据,同步更新 Redux pagination.current_page
|
||||
dispatch(updatePaginationPage({ mode, page: newPage }));
|
||||
} else {
|
||||
console.log(`%c❌ [缓存] 第${newPage}页未缓存,加载数据`, 'color: #DC2626; font-weight: bold;');
|
||||
// clearCache = false:API 会更新 Redux pagination.current_page
|
||||
await loadPage(newPage, false);
|
||||
}
|
||||
} else {
|
||||
// 平铺模式:直接加载新页(追加模式,clearCache=false)
|
||||
console.log(`%c🟡 [平铺模式] 加载第${newPage}页`, 'color: #EAB308; font-weight: bold;');
|
||||
// clearCache = false:API 会更新 Redux pagination.current_page
|
||||
await loadPage(newPage, false);
|
||||
}
|
||||
}, [mode, currentPage, totalPages, loadingPage, allCachedEventsByPage, loadPage]);
|
||||
|
||||
|
||||
// 加载下一页(用于无限滚动)
|
||||
const loadNextPage = useCallback(async () => {
|
||||
// 使用 hasMore 判断(基于 currentPage < totalPages)
|
||||
if (!hasMore || loadingPage !== null) {
|
||||
logger.debug('DynamicNewsCard', '无法加载下一页', {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasMore,
|
||||
loadingPage,
|
||||
reason: !hasMore ? '已加载全部数据 (currentPage >= totalPages)' : '正在加载中'
|
||||
});
|
||||
return Promise.resolve(false); // 没有更多数据或正在加载
|
||||
}
|
||||
|
||||
const nextPage = currentPage + 1;
|
||||
logger.debug('DynamicNewsCard', '懒加载:加载下一页', { currentPage, nextPage, hasMore, totalPages });
|
||||
|
||||
try {
|
||||
await handlePageChange(nextPage);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DynamicNewsCard', '懒加载失败', error, { nextPage });
|
||||
return false;
|
||||
}
|
||||
}, [currentPage, totalPages, hasMore, loadingPage, handlePageChange]);
|
||||
|
||||
// 加载上一页(用于双向无限滚动)
|
||||
const loadPrevPage = useCallback(async () => {
|
||||
if (currentPage <= 1 || loadingPage !== null) {
|
||||
logger.debug('DynamicNewsCard', '无法加载上一页', {
|
||||
currentPage,
|
||||
loadingPage,
|
||||
reason: currentPage <= 1 ? '已是第一页' : '正在加载中'
|
||||
});
|
||||
return Promise.resolve(false); // 已经是第一页或正在加载
|
||||
}
|
||||
|
||||
const prevPage = currentPage - 1;
|
||||
logger.debug('DynamicNewsCard', '懒加载:加载上一页', { currentPage, prevPage });
|
||||
|
||||
try {
|
||||
await handlePageChange(prevPage);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DynamicNewsCard', '懒加载上一页失败', error, { prevPage });
|
||||
return false;
|
||||
}
|
||||
}, [currentPage, loadingPage, handlePageChange]);
|
||||
|
||||
// 模式切换处理(简化版 - 模式切换时始终请求数据,因为两种模式使用独立存储)
|
||||
const handleModeToggle = useCallback((newMode) => {
|
||||
if (newMode === mode) return;
|
||||
|
||||
setMode(newMode);
|
||||
// currentPage 由 Redux pagination.current_page 派生,会在下次请求时自动更新
|
||||
// pageSize 会根据 mode 自动重新计算(第46-56行)
|
||||
}, [mode]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentPage,
|
||||
mode,
|
||||
loadingPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasMore,
|
||||
currentPageEvents,
|
||||
displayEvents, // 当前显示的事件列表
|
||||
|
||||
// 方法
|
||||
handlePageChange,
|
||||
handleModeToggle,
|
||||
loadNextPage, // 加载下一页(用于无限滚动)
|
||||
loadPrevPage // 加载上一页(用于双向无限滚动)
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user