feat: feat: 优化事件卡片 UI 和交互体验
修复 useColorModeValue 调用位置(提升到顶层) 优化分页和滚动逻辑 动态 indicatorSize 支持(detail/list 模式)
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
// 状态
|
// 状态
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user