170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||
import { useEffect, useRef, useState } from 'react';
|
||
|
||
interface ScrollableRowProps {
|
||
children: React.ReactNode;
|
||
scrollDistance?: number;
|
||
}
|
||
|
||
export default function ScrollableRow({
|
||
children,
|
||
scrollDistance = 1000,
|
||
}: 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: scrollDistance,
|
||
behavior: 'smooth',
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleScrollLeftClick = () => {
|
||
if (containerRef.current) {
|
||
containerRef.current.scrollBy({
|
||
left: -scrollDistance,
|
||
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 py-1 sm:py-2 pb-12 sm:pb-14 px-4 sm:px-6'
|
||
onScroll={checkScroll}
|
||
>
|
||
{children}
|
||
</div>
|
||
{showLeftScroll && (
|
||
<div
|
||
className={`hidden sm:flex absolute left-0 top-0 bottom-0 w-16 items-center justify-center z-[600] transition-opacity duration-200 ${
|
||
isHovered ? 'opacity-100' : 'opacity-0'
|
||
}`}
|
||
style={{
|
||
background: 'transparent',
|
||
pointerEvents: 'none', // 允许点击穿透
|
||
}}
|
||
>
|
||
<div
|
||
className='absolute inset-0 flex items-center justify-center'
|
||
style={{
|
||
top: '40%',
|
||
bottom: '60%',
|
||
left: '-4.5rem',
|
||
pointerEvents: 'auto',
|
||
}}
|
||
>
|
||
<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 dark:bg-gray-800/90 dark:hover:bg-gray-700 dark:border-gray-600'
|
||
>
|
||
<ChevronLeft className='w-6 h-6 text-gray-600 dark:text-gray-300' />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showRightScroll && (
|
||
<div
|
||
className={`hidden sm:flex absolute right-0 top-0 bottom-0 w-16 items-center justify-center z-[600] transition-opacity duration-200 ${
|
||
isHovered ? 'opacity-100' : 'opacity-0'
|
||
}`}
|
||
style={{
|
||
background: 'transparent',
|
||
pointerEvents: 'none', // 允许点击穿透
|
||
}}
|
||
>
|
||
<div
|
||
className='absolute inset-0 flex items-center justify-center'
|
||
style={{
|
||
top: '40%',
|
||
bottom: '60%',
|
||
right: '-4.5rem',
|
||
pointerEvents: 'auto',
|
||
}}
|
||
>
|
||
<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 dark:bg-gray-800/90 dark:hover:bg-gray-700 dark:border-gray-600'
|
||
>
|
||
<ChevronRight className='w-6 h-6 text-gray-600 dark:text-gray-300' />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|