feat: feat: 优化事件卡片 UI 和交互体验

修复 useColorModeValue 调用位置(提升到顶层)
优化分页和滚动逻辑
动态 indicatorSize 支持(detail/list 模式)
This commit is contained in:
zdl
2025-11-05 19:15:36 +08:00
parent 27b68e928e
commit 612b58c983
4 changed files with 66 additions and 68 deletions

View File

@@ -1,7 +1,7 @@
// src/views/Community/components/DynamicNewsCard/EventScrollList.js // src/views/Community/components/DynamicNewsCard/EventScrollList.js
// 横向滚动事件列表组件 // 横向滚动事件列表组件
import React, { useRef } from 'react'; import React, { useRef, useCallback } from 'react';
import { import {
Box, Box,
Flex, Flex,
@@ -44,6 +44,7 @@ const EventScrollList = ({
totalPages, totalPages,
onPageChange, onPageChange,
loading = false, loading = false,
error, // 错误状态
mode = 'vertical', mode = 'vertical',
onModeChange, onModeChange,
hasMore = true, hasMore = true,
@@ -72,20 +73,27 @@ const EventScrollList = ({
}; };
}; };
// 重试函数
const handleRetry = useCallback(() => {
if (onPageChange) {
onPageChange(currentPage);
}
}, [onPageChange, currentPage]);
return ( return (
<Box> <Box>
{/* 顶部控制栏:模式切换按钮(左)+ 分页控制器(右) */} {/* 顶部控制栏:模式切换按钮 + 分页控制器 */}
<Flex justify="space-between" align="center" mb={2}> <Flex justify="space-between" align="center" mb={2}>
{/* 模式切换按钮 */} {/* 左侧:模式切换按钮 */}
<ModeToggleButtons mode={mode} onModeChange={onModeChange} /> <ModeToggleButtons mode={mode} onModeChange={onModeChange} />
{/* 分页控制器(平铺模式显示,使用无限滚动 */} {/* 右侧:分页控制器(纵向和平铺模式显示) */}
{totalPages > 1 && mode !== 'four-row' && ( {totalPages > 1 && (
<PaginationControl <PaginationControl
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={onPageChange} onPageChange={onPageChange}
/> />
)} )}
</Flex> </Flex>
@@ -116,11 +124,9 @@ const EventScrollList = ({
<Box <Box
ref={scrollContainerRef} ref={scrollContainerRef}
overflowX="hidden" overflowX="hidden"
overflowY="hidden"
maxH={mode === 'vertical' ? '820px' : 'none'}
pt={0} pt={0}
pb={4} pb={4}
px={2} px={mode === 'four-row' ? 0 : 2}
position="relative" position="relative"
css={{ css={{
// 统一滚动条样式(支持横向和纵向) // 统一滚动条样式(支持横向和纵向)
@@ -146,6 +152,7 @@ const EventScrollList = ({
{/* 平铺网格模式 - 使用虚拟滚动 + 双向无限滚动 */} {/* 平铺网格模式 - 使用虚拟滚动 + 双向无限滚动 */}
{mode === 'four-row' && ( {mode === 'four-row' && (
<VirtualizedFourRowGrid <VirtualizedFourRowGrid
columnsPerRow={4} // 每行显示4列
events={displayEvents || events} // 使用累积列表(如果有) events={displayEvents || events} // 使用累积列表(如果有)
selectedEvent={selectedEvent} selectedEvent={selectedEvent}
onEventSelect={onFourRowEventClick} // 四排模式点击打开弹窗 onEventSelect={onFourRowEventClick} // 四排模式点击打开弹窗
@@ -157,6 +164,8 @@ const EventScrollList = ({
loadPrevPage={loadPrevPage} // 加载上一页(双向滚动) loadPrevPage={loadPrevPage} // 加载上一页(双向滚动)
hasMore={hasMore} // 是否还有更多数据 hasMore={hasMore} // 是否还有更多数据
loading={loading} // 加载状态 loading={loading} // 加载状态
error={error} // 错误状态
onRetry={handleRetry} // 重试回调
/> />
)} )}
@@ -170,9 +179,9 @@ const EventScrollList = ({
onToggleFollow={onToggleFollow} onToggleFollow={onToggleFollow}
getTimelineBoxStyle={getTimelineBoxStyle} getTimelineBoxStyle={getTimelineBoxStyle}
borderColor={borderColor} borderColor={borderColor}
scrollbarTrackBg={scrollbarTrackBg} currentPage={currentPage}
scrollbarThumbBg={scrollbarThumbBg} totalPages={totalPages}
scrollbarThumbHoverBg={scrollbarThumbHoverBg} onPageChange={onPageChange}
/> />
)} )}
</Box> </Box>

View File

@@ -34,16 +34,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
// 根据模式决定每页显示数量 // 根据模式决定每页显示数量
const pageSize = (() => { const pageSize = (() => {
switch (mode) { switch (mode) {
case DISPLAY_MODES.CAROUSEL:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
case DISPLAY_MODES.GRID:
return PAGINATION_CONFIG.GRID_PAGE_SIZE;
case DISPLAY_MODES.FOUR_ROW: case DISPLAY_MODES.FOUR_ROW:
return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE; return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE;
case DISPLAY_MODES.VERTICAL: case DISPLAY_MODES.VERTICAL:
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
default: default:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE; return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
} }
})(); })();
@@ -126,20 +122,37 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
try { try {
console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;'); console.log(`%c🟢 [API请求] 开始加载第${targetPage}页数据`, 'color: #16A34A; font-weight: bold;');
console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}`, 'color: #16A34A;'); console.log(`%c 请求参数: page=${targetPage}, per_page=${pageSize}, mode=${mode}`, 'color: #16A34A;');
console.log(`%c 筛选条件:`, 'color: #16A34A;', filters);
logger.debug('DynamicNewsCard', '开始加载页面数据', { logger.debug('DynamicNewsCard', '开始加载页面数据', {
targetPage, targetPage,
pageSize pageSize,
mode,
filters
}); });
await dispatch(fetchDynamicNews({ // 🔍 调试dispatch 前
console.log(`%c🔵 [dispatch] 准备调用 fetchDynamicNews`, 'color: #3B82F6; font-weight: bold;', {
mode,
page: targetPage, page: targetPage,
per_page: pageSize, per_page: pageSize,
pageSize,
clearCache: false,
filters
});
const result = await dispatch(fetchDynamicNews({
mode: mode, // 传递 mode 参数
per_page: pageSize,
pageSize: pageSize, pageSize: pageSize,
clearCache: false clearCache: false,
...filters, // 先展开筛选条件
page: targetPage, // 然后覆盖 page 参数(避免被 filters.page 覆盖)
})).unwrap(); })).unwrap();
// 🔍 调试dispatch 后
console.log(`%c🔵 [dispatch] fetchDynamicNews 返回结果`, 'color: #3B82F6; font-weight: bold;', result);
console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;'); console.log(`%c🟢 [API请求] 第${targetPage}页加载完成`, 'color: #16A34A; font-weight: bold;');
logger.debug('DynamicNewsCard', `${targetPage} 页加载完成`); logger.debug('DynamicNewsCard', `${targetPage} 页加载完成`);
@@ -185,10 +198,12 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
logger.debug('DynamicNewsCard', '返回第一页,清空缓存重新加载'); logger.debug('DynamicNewsCard', '返回第一页,清空缓存重新加载');
setCurrentPage(1); setCurrentPage(1);
dispatch(fetchDynamicNews({ dispatch(fetchDynamicNews({
page: 1, mode: mode, // 传递 mode 参数
per_page: pageSize, per_page: pageSize,
pageSize: pageSize, pageSize: pageSize,
clearCache: true // 清空缓存 clearCache: true, // 清空缓存
...filters, // 先展开筛选条件
page: 1, // 然后覆盖 page 参数
})); }));
return; return;
} }
@@ -305,7 +320,7 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
} }
}, [currentPage, loadingPage, handlePageChange]); }, [currentPage, loadingPage, handlePageChange]);
// 模式切换处理 // 模式切换处理(简化版 - 模式切换时始终请求数据,因为两种模式使用独立存储)
const handleModeToggle = useCallback((newMode) => { const handleModeToggle = useCallback((newMode) => {
if (newMode === mode) return; if (newMode === mode) return;
@@ -314,36 +329,15 @@ export const usePagination = ({ allCachedEvents, total, cachedCount, dispatch, t
const newPageSize = (() => { const newPageSize = (() => {
switch (newMode) { switch (newMode) {
case DISPLAY_MODES.CAROUSEL:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE;
case DISPLAY_MODES.GRID:
return PAGINATION_CONFIG.GRID_PAGE_SIZE;
case DISPLAY_MODES.FOUR_ROW: case DISPLAY_MODES.FOUR_ROW:
return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE; return PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE;
case DISPLAY_MODES.VERTICAL: case DISPLAY_MODES.VERTICAL:
return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
default: default:
return PAGINATION_CONFIG.CAROUSEL_PAGE_SIZE; return PAGINATION_CONFIG.VERTICAL_PAGE_SIZE;
} }
})(); })();
}, [mode]);
// 检查第1页的数据是否完整排除 null
const firstPageData = allCachedEvents.slice(0, newPageSize);
const validFirstPageCount = firstPageData.filter(e => e !== null).length;
const needsRefetch = validFirstPageCount < Math.min(newPageSize, total);
if (needsRefetch) {
// 第1页数据不完整清空缓存重新请求
dispatch(fetchDynamicNews({
page: 1,
per_page: newPageSize,
pageSize: newPageSize, // 传递 pageSize 确保索引计算一致
clearCache: true,
...filters // 应用筛选条件
}));
}
// 如果第1页数据完整不发起请求直接切换
}, [mode, allCachedEvents, total, dispatch, filters]);
return { return {
// 状态 // 状态

View File

@@ -33,6 +33,7 @@ import StockChangeIndicators from '../../../../components/StockChangeIndicators'
* @param {Function} props.onToggleFollow - 切换关注事件 * @param {Function} props.onToggleFollow - 切换关注事件
* @param {Object} props.timelineStyle - 时间轴样式配置 * @param {Object} props.timelineStyle - 时间轴样式配置
* @param {string} props.borderColor - 边框颜色 * @param {string} props.borderColor - 边框颜色
* @param {string} props.indicatorSize - 涨幅指标尺寸 ('default' | 'comfortable' | 'large')
*/ */
const HorizontalDynamicNewsEventCard = ({ const HorizontalDynamicNewsEventCard = ({
event, event,
@@ -45,9 +46,15 @@ const HorizontalDynamicNewsEventCard = ({
onToggleFollow, onToggleFollow,
timelineStyle, timelineStyle,
borderColor, borderColor,
indicatorSize = 'comfortable',
}) => { }) => {
const importance = getImportanceConfig(event.importance); const importance = getImportanceConfig(event.importance);
// 所有 useColorModeValue 必须在顶层调用
const cardBg = useColorModeValue('white', 'gray.800'); const cardBg = useColorModeValue('white', 'gray.800');
const cardBgAlt = useColorModeValue('gray.50', 'gray.750');
const selectedBg = useColorModeValue('blue.50', 'blue.900');
const selectedBorderColor = useColorModeValue('blue.500', 'blue.400');
const linkColor = useColorModeValue('blue.600', 'blue.400'); const linkColor = useColorModeValue('blue.600', 'blue.400');
return ( return (
@@ -65,12 +72,12 @@ const HorizontalDynamicNewsEventCard = ({
flex="1" flex="1"
position="relative" position="relative"
bg={isSelected bg={isSelected
? useColorModeValue('blue.50', 'blue.900') ? selectedBg
: (index % 2 === 0 ? cardBg : useColorModeValue('gray.50', 'gray.750')) : (index % 2 === 0 ? cardBg : cardBgAlt)
} }
borderWidth={isSelected ? "2px" : "1px"} borderWidth={isSelected ? "2px" : "1px"}
borderColor={isSelected borderColor={isSelected
? useColorModeValue('blue.500', 'blue.400') ? selectedBorderColor
: borderColor : borderColor
} }
borderRadius="md" borderRadius="md"
@@ -137,7 +144,7 @@ const HorizontalDynamicNewsEventCard = ({
avgChange={event.related_avg_chg} avgChange={event.related_avg_chg}
maxChange={event.related_max_chg} maxChange={event.related_max_chg}
weekChange={event.related_week_chg} weekChange={event.related_week_chg}
size="comfortable" size={indicatorSize}
/> />
</VStack> </VStack>
</CardBody> </CardBody>

View File

@@ -5,8 +5,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { import {
fetchPopularKeywords, fetchPopularKeywords,
fetchHotEvents, fetchHotEvents,
fetchDynamicNews, fetchDynamicNews
selectDynamicNewsWithLoading
} from '../../store/slices/communityDataSlice'; } from '../../store/slices/communityDataSlice';
import { import {
Box, Box,
@@ -39,13 +38,6 @@ const Community = () => {
// Redux状态 // Redux状态
const { popularKeywords, hotEvents } = useSelector(state => state.communityData); const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
const {
data: allCachedEvents,
loading: dynamicNewsLoading,
error: dynamicNewsError,
total: dynamicNewsTotal,
cachedCount: dynamicNewsCachedCount
} = useSelector(selectDynamicNewsWithLoading);
// Chakra UI hooks // Chakra UI hooks
const bgColor = useColorModeValue('gray.50', 'gray.900'); const bgColor = useColorModeValue('gray.50', 'gray.900');
@@ -167,10 +159,6 @@ const Community = () => {
{/* 实时要闻·动态追踪 - 横向滚动 */} {/* 实时要闻·动态追踪 - 横向滚动 */}
<DynamicNewsCard <DynamicNewsCard
mt={6} mt={6}
allCachedEvents={allCachedEvents}
loading={dynamicNewsLoading}
total={dynamicNewsTotal}
cachedCount={dynamicNewsCachedCount}
filters={filters} filters={filters}
popularKeywords={popularKeywords} popularKeywords={popularKeywords}
lastUpdateTime={lastUpdateTime} lastUpdateTime={lastUpdateTime}