mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-16 13:14:43 +08:00
first commit
This commit is contained in:
169
src/components/ScrollableRow.tsx
Normal file
169
src/components/ScrollableRow.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user