feat: scroll x

This commit is contained in:
shinya
2025-06-18 02:25:49 +08:00
parent 479fafcb1c
commit 9c16d5636c
8 changed files with 233 additions and 29 deletions

View File

@@ -0,0 +1,155 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
interface ScrollableRowProps {
children: React.ReactNode;
}
export default function ScrollableRow({ children }: ScrollableRowProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [showLeftScroll, setShowLeftScroll] = useState(false);
const [showRightScroll, setShowRightScroll] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const checkScroll = () => {
if (containerRef.current) {
const { scrollWidth, clientWidth, scrollLeft } = containerRef.current;
// 计算是否需要左右滚动按钮
const threshold = 1; // 容差值,避免浮点误差
const canScrollRight =
scrollWidth - (scrollLeft + clientWidth) > threshold;
const canScrollLeft = scrollLeft > threshold;
setShowRightScroll(canScrollRight);
setShowLeftScroll(canScrollLeft);
}
};
useEffect(() => {
// 多次延迟检查,确保内容已完全渲染
checkScroll();
// 监听窗口大小变化
window.addEventListener('resize', checkScroll);
// 创建一个 ResizeObserver 来监听容器大小变化
const resizeObserver = new ResizeObserver(() => {
// 延迟执行检查
checkScroll();
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
window.removeEventListener('resize', checkScroll);
resizeObserver.disconnect();
};
}, [children]); // 依赖 children当子组件变化时重新检查
// 添加一个额外的效果来监听子组件的变化
useEffect(() => {
if (containerRef.current) {
// 监听 DOM 变化
const observer = new MutationObserver(() => {
setTimeout(checkScroll, 100);
});
observer.observe(containerRef.current, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
return () => observer.disconnect();
}
}, []);
const handleScrollRightClick = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: 800, behavior: 'smooth' });
}
};
const handleScrollLeftClick = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: -800, behavior: 'smooth' });
}
};
return (
<div
className='relative'
onMouseEnter={() => {
setIsHovered(true);
// 当鼠标进入时重新检查一次
checkScroll();
}}
onMouseLeave={() => setIsHovered(false)}
>
<div
ref={containerRef}
className='flex space-x-6 overflow-x-auto scrollbar-hide pb-14'
onScroll={checkScroll}
>
{children}
</div>
{showLeftScroll && (
<div
className={`absolute left-0 top-0 bottom-0 w-16 flex items-center justify-center z-50 transition-opacity duration-200 ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
style={{
background: 'transparent',
}}
>
<div
className='absolute inset-0'
onClick={(e) => e.stopPropagation()}
/>
<div
className='absolute inset-0 flex items-center justify-center'
style={{ top: '40%', bottom: '60%' }}
>
<button
onClick={handleScrollLeftClick}
className='w-12 h-12 bg-white/95 rounded-full shadow-lg flex items-center justify-center hover:bg-white border border-gray-200 transition-transform hover:scale-105'
>
<ChevronLeft className='w-6 h-6 text-gray-600' />
</button>
</div>
</div>
)}
{showRightScroll && (
<div
className={`absolute right-0 top-0 bottom-0 w-16 flex items-center justify-center z-50 transition-opacity duration-200 ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
style={{
background: 'transparent',
}}
>
<div
className='absolute inset-0'
onClick={(e) => e.stopPropagation()}
/>
<div
className='absolute inset-0 flex items-center justify-center'
style={{ top: '40%', bottom: '60%' }}
>
<button
onClick={handleScrollRightClick}
className='w-12 h-12 bg-white/95 rounded-full shadow-lg flex items-center justify-center hover:bg-white border border-gray-200 transition-transform hover:scale-105'
>
<ChevronRight className='w-6 h-6 text-gray-600' />
</button>
</div>
</div>
)}
</div>
);
}