feat: 优化社区页面滚动和分页交互体验…)
⎿ [feature_2025/1028_event 5dedbb3] feat: 优化社区页面滚动和分页交互体验
6 files changed, 1355 insertions(+), 49 deletions(-)
create mode 100644 docs/test-cases/Community351241265351235242346265213350257225347224250344276213.md
This commit is contained in:
@@ -72,8 +72,8 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
// 发起 Redux action 获取新页面数据
|
// 发起 Redux action 获取新页面数据
|
||||||
dispatch(fetchDynamicNews({ page: newPage, per_page: pageSize }));
|
dispatch(fetchDynamicNews({ page: newPage, per_page: pageSize }));
|
||||||
|
|
||||||
// 重置选中事件(等新数据加载后自动选中第一个)
|
// 保持当前选中事件,避免详情面板消失导致页面抖动
|
||||||
setSelectedEvent(null);
|
// 新数据加载完成后,useEffect 会自动选中第一个事件
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -112,27 +112,8 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
|
|
||||||
{/* 主体内容 */}
|
{/* 主体内容 */}
|
||||||
<CardBody position="relative">
|
<CardBody position="relative">
|
||||||
{/* Loading 状态 */}
|
{/* 横向滚动事件列表 - 始终渲染(除非为空) */}
|
||||||
{loading && (
|
{events && events.length > 0 ? (
|
||||||
<Center py={10}>
|
|
||||||
<VStack>
|
|
||||||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
|
||||||
<Text color="gray.500">正在加载最新事件...</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty 状态 */}
|
|
||||||
{!loading && (!events || events.length === 0) && (
|
|
||||||
<Center py={10}>
|
|
||||||
<VStack>
|
|
||||||
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 横向滚动事件列表 */}
|
|
||||||
{!loading && events && events.length > 0 && (
|
|
||||||
<EventScrollList
|
<EventScrollList
|
||||||
events={events}
|
events={events}
|
||||||
selectedEvent={selectedEvent}
|
selectedEvent={selectedEvent}
|
||||||
@@ -141,11 +122,27 @@ const DynamicNewsCard = forwardRef(({
|
|||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
) : !loading ? (
|
||||||
|
/* Empty 状态 - 只在非加载且无数据时显示 */
|
||||||
|
<Center py={10}>
|
||||||
|
<VStack>
|
||||||
|
<Text fontSize="lg" color="gray.500">暂无事件数据</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
/* 首次加载状态 */
|
||||||
|
<Center py={10}>
|
||||||
|
<VStack>
|
||||||
|
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||||||
|
<Text color="gray.500">正在加载最新事件...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 详情面板 */}
|
{/* 详情面板 - 始终显示(如果有选中事件) */}
|
||||||
{!loading && events && events.length > 0 && (
|
{events && events.length > 0 && selectedEvent && (
|
||||||
<Box mt={6}>
|
<Box mt={6}>
|
||||||
<DynamicNewsDetailPanel event={selectedEvent} />
|
<DynamicNewsDetailPanel event={selectedEvent} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Center,
|
||||||
|
VStack,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
useColorModeValue
|
useColorModeValue
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||||
@@ -21,6 +25,7 @@ import PaginationControl from './PaginationControl';
|
|||||||
* @param {number} currentPage - 当前页码
|
* @param {number} currentPage - 当前页码
|
||||||
* @param {number} totalPages - 总页数(由服务端返回)
|
* @param {number} totalPages - 总页数(由服务端返回)
|
||||||
* @param {Function} onPageChange - 页码改变回调
|
* @param {Function} onPageChange - 页码改变回调
|
||||||
|
* @param {boolean} loading - 加载状态
|
||||||
*/
|
*/
|
||||||
const EventScrollList = ({
|
const EventScrollList = ({
|
||||||
events,
|
events,
|
||||||
@@ -29,7 +34,8 @@ const EventScrollList = ({
|
|||||||
borderColor,
|
borderColor,
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
onPageChange
|
onPageChange,
|
||||||
|
loading = false
|
||||||
}) => {
|
}) => {
|
||||||
const scrollContainerRef = useRef(null);
|
const scrollContainerRef = useRef(null);
|
||||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||||
@@ -89,17 +95,6 @@ const EventScrollList = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 分页控制器 - 右上角 */}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<Flex justify="flex-end" mb={3}>
|
|
||||||
<PaginationControl
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
onPageChange={onPageChange}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 横向滚动区域 */}
|
{/* 横向滚动区域 */}
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
{/* 左侧滚动按钮 */}
|
{/* 左侧滚动按钮 */}
|
||||||
@@ -149,6 +144,7 @@ const EventScrollList = ({
|
|||||||
py={4}
|
py={4}
|
||||||
px={2}
|
px={2}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
position="relative"
|
||||||
css={{
|
css={{
|
||||||
'&::-webkit-scrollbar': {
|
'&::-webkit-scrollbar': {
|
||||||
height: '8px',
|
height: '8px',
|
||||||
@@ -170,6 +166,29 @@ const EventScrollList = ({
|
|||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 加载遮罩 */}
|
||||||
|
{loading && (
|
||||||
|
<Center
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
bg={useColorModeValue('whiteAlpha.800', 'blackAlpha.700')}
|
||||||
|
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>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 事件卡片列表 */}
|
||||||
{events.map((event, index) => (
|
{events.map((event, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@@ -199,6 +218,17 @@ const EventScrollList = ({
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 分页控制器 - 右下角 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Flex justify="flex-end" mt={3}>
|
||||||
|
<PaginationControl
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={onPageChange}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,17 +102,7 @@ export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {
|
|||||||
|
|
||||||
// 保持现有筛选条件,只更新页码
|
// 保持现有筛选条件,只更新页码
|
||||||
updateFilters({ ...filters, page });
|
updateFilters({ ...filters, page });
|
||||||
|
}, [filters, updateFilters, track]);
|
||||||
// 滚动到实时事件时间轴(平滑滚动)
|
|
||||||
if (eventTimelineRef && eventTimelineRef.current) {
|
|
||||||
setTimeout(() => {
|
|
||||||
eventTimelineRef.current.scrollIntoView({
|
|
||||||
behavior: 'smooth', // 平滑滚动
|
|
||||||
block: 'start' // 滚动到元素顶部
|
|
||||||
});
|
|
||||||
}, 100); // 延迟100ms,确保DOM更新
|
|
||||||
}
|
|
||||||
}, [filters, updateFilters, eventTimelineRef, track]);
|
|
||||||
|
|
||||||
// 处理事件点击
|
// 处理事件点击
|
||||||
const handleEventClick = useCallback((event) => {
|
const handleEventClick = useCallback((event) => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const Community = () => {
|
|||||||
// Ref:用于滚动到实时事件时间轴
|
// Ref:用于滚动到实时事件时间轴
|
||||||
const eventTimelineRef = useRef(null);
|
const eventTimelineRef = useRef(null);
|
||||||
const hasScrolledRef = useRef(false); // 标记是否已滚动
|
const hasScrolledRef = useRef(false); // 标记是否已滚动
|
||||||
|
const containerRef = useRef(null); // 用于首次滚动到内容区域
|
||||||
|
|
||||||
// ⚡ 通知权限引导
|
// ⚡ 通知权限引导
|
||||||
const { showCommunityGuide } = useNotification();
|
const { showCommunityGuide } = useNotification();
|
||||||
@@ -145,6 +146,23 @@ const Community = () => {
|
|||||||
}
|
}
|
||||||
}, [showCommunityGuide]); // 只在组件挂载时执行一次
|
}, [showCommunityGuide]); // 只在组件挂载时执行一次
|
||||||
|
|
||||||
|
// ⚡ 首次进入页面时滚动到内容区域(考虑导航栏高度)
|
||||||
|
useEffect(() => {
|
||||||
|
// 延迟执行,确保DOM已完全渲染
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
// 滚动到容器顶部,自动考虑导航栏的高度
|
||||||
|
containerRef.current.scrollIntoView({
|
||||||
|
behavior: 'auto',
|
||||||
|
block: 'start',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||||||
|
|
||||||
// ⚡ 滚动到实时事件区域(由搜索框聚焦触发)
|
// ⚡ 滚动到实时事件区域(由搜索框聚焦触发)
|
||||||
const scrollToTimeline = useCallback(() => {
|
const scrollToTimeline = useCallback(() => {
|
||||||
if (!hasScrolledRef.current && eventTimelineRef.current) {
|
if (!hasScrolledRef.current && eventTimelineRef.current) {
|
||||||
@@ -161,7 +179,7 @@ const Community = () => {
|
|||||||
return (
|
return (
|
||||||
<Box minH="100vh" bg={bgColor}>
|
<Box minH="100vh" bg={bgColor}>
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<Container maxW="container.xl" pt={6} pb={8}>
|
<Container ref={containerRef} maxW="container.xl" pt={6} pb={8}>
|
||||||
{/* 热点事件区域 */}
|
{/* 热点事件区域 */}
|
||||||
<HotEventsSection events={hotEvents} />
|
<HotEventsSection events={hotEvents} />
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,9 @@ const EventDetail = () => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
||||||
|
|
||||||
|
// 滚动位置管理
|
||||||
|
const scrollPositionRef = useRef(0);
|
||||||
|
|
||||||
// State hooks
|
// State hooks
|
||||||
const [eventData, setEventData] = useState(null);
|
const [eventData, setEventData] = useState(null);
|
||||||
const [relatedStocks, setRelatedStocks] = useState([]);
|
const [relatedStocks, setRelatedStocks] = useState([]);
|
||||||
@@ -399,6 +402,16 @@ const EventDetail = () => {
|
|||||||
|
|
||||||
const actualEventId = getEventIdFromPath();
|
const actualEventId = getEventIdFromPath();
|
||||||
|
|
||||||
|
// 保存当前滚动位置
|
||||||
|
const saveScrollPosition = () => {
|
||||||
|
scrollPositionRef.current = window.scrollY || window.pageYOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 恢复滚动位置
|
||||||
|
const restoreScrollPosition = () => {
|
||||||
|
window.scrollTo(0, scrollPositionRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
const loadEventData = async () => {
|
const loadEventData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -540,8 +553,19 @@ const EventDetail = () => {
|
|||||||
// Effect hook - must be called after all state hooks
|
// Effect hook - must be called after all state hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (actualEventId) {
|
if (actualEventId) {
|
||||||
|
// 保存当前滚动位置
|
||||||
|
saveScrollPosition();
|
||||||
|
|
||||||
loadEventData();
|
loadEventData();
|
||||||
loadPosts();
|
loadPosts();
|
||||||
|
|
||||||
|
// 数据加载完成后恢复滚动位置
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
restoreScrollPosition();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
} else {
|
} else {
|
||||||
setError('无效的事件ID');
|
setError('无效的事件ID');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user