新增源站寻片

This commit is contained in:
mtvpls
2025-12-31 17:38:06 +08:00
parent caf8f28df3
commit 8e9eb76d58
9 changed files with 984 additions and 54 deletions

View File

@@ -16,7 +16,14 @@ const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
className,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const isScrollingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isDraggingRef = useRef(false);
const startXRef = useRef(0);
const scrollLeftRef = useRef(0);
const hasDraggedRef = useRef(false);
const [indicatorStyle, setIndicatorStyle] = useState<{
left: number;
width: number;
@@ -24,79 +31,205 @@ const CapsuleSwitch: React.FC<CapsuleSwitchProps> = ({
const activeIndex = options.findIndex((opt) => opt.value === active);
// 更新指示器位置
const updateIndicatorPosition = () => {
// 更新指示器位置(仅更新位置,不触发滚动)
const updateIndicatorPosition = (autoScroll = false) => {
if (
activeIndex >= 0 &&
buttonRefs.current[activeIndex] &&
containerRef.current
containerRef.current &&
scrollContainerRef.current
) {
const button = buttonRefs.current[activeIndex];
const container = containerRef.current;
if (button && container) {
const buttonRect = button.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const scrollContainer = scrollContainerRef.current;
if (buttonRect.width > 0) {
setIndicatorStyle({
left: buttonRect.left - containerRect.left,
width: buttonRect.width,
});
if (button) {
const buttonOffsetLeft = button.offsetLeft;
const buttonWidth = button.offsetWidth;
setIndicatorStyle({
left: buttonOffsetLeft,
width: buttonWidth,
});
// 只在需要自动滚动时才执行
if (autoScroll && !isScrollingRef.current) {
const buttonRect = button.getBoundingClientRect();
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const isVisible =
buttonRect.left >= scrollContainerRect.left &&
buttonRect.right <= scrollContainerRect.right;
if (!isVisible) {
// 将选中项滚动到视图中心
const scrollToPosition =
buttonOffsetLeft -
scrollContainer.offsetWidth / 2 +
buttonWidth / 2;
scrollContainer.scrollTo({
left: scrollToPosition,
behavior: 'smooth',
});
}
}
}
}
};
// 组件挂载时立即计算初始位置
// 组件挂载时立即计算初始位置并滚动到选中项
useEffect(() => {
const timeoutId = setTimeout(updateIndicatorPosition, 0);
const timeoutId = setTimeout(() => updateIndicatorPosition(true), 0);
return () => clearTimeout(timeoutId);
}, []);
// 监听选中项变化
// 监听选中项变化,自动滚动到新选中项
useEffect(() => {
const timeoutId = setTimeout(updateIndicatorPosition, 0);
const timeoutId = setTimeout(() => updateIndicatorPosition(true), 0);
return () => clearTimeout(timeoutId);
}, [activeIndex]);
// 监听滚动事件,仅更新指示器位置
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
const handleScroll = () => {
// 标记正在滚动
isScrollingRef.current = true;
// 清除之前的超时
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
// 仅更新指示器位置,不触发自动滚动
updateIndicatorPosition(false);
// 滚动结束后重置标记
scrollTimeoutRef.current = setTimeout(() => {
isScrollingRef.current = false;
}, 150);
};
scrollContainer.addEventListener('scroll', handleScroll, { passive: true });
return () => {
scrollContainer.removeEventListener('scroll', handleScroll);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
};
}, [activeIndex]);
// 鼠标拖动功能
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
const handleMouseDown = (e: MouseEvent) => {
isDraggingRef.current = true;
hasDraggedRef.current = false;
startXRef.current = e.pageX - scrollContainer.offsetLeft;
scrollLeftRef.current = scrollContainer.scrollLeft;
scrollContainer.style.cursor = 'grabbing';
scrollContainer.style.userSelect = 'none';
};
const handleMouseLeave = () => {
isDraggingRef.current = false;
scrollContainer.style.cursor = 'grab';
};
const handleMouseUp = () => {
isDraggingRef.current = false;
scrollContainer.style.cursor = 'grab';
// 短暂延迟后重置拖动标记,防止点击事件被触发
setTimeout(() => {
hasDraggedRef.current = false;
}, 50);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDraggingRef.current) return;
e.preventDefault();
const x = e.pageX - scrollContainer.offsetLeft;
const walk = (x - startXRef.current) * 1.5; // 调整拖动速度
// 如果移动距离超过5px标记为已拖动
if (Math.abs(walk) > 5) {
hasDraggedRef.current = true;
}
scrollContainer.scrollLeft = scrollLeftRef.current - walk;
};
scrollContainer.style.cursor = 'grab';
scrollContainer.addEventListener('mousedown', handleMouseDown);
scrollContainer.addEventListener('mouseleave', handleMouseLeave);
scrollContainer.addEventListener('mouseup', handleMouseUp);
scrollContainer.addEventListener('mousemove', handleMouseMove);
return () => {
scrollContainer.removeEventListener('mousedown', handleMouseDown);
scrollContainer.removeEventListener('mouseleave', handleMouseLeave);
scrollContainer.removeEventListener('mouseup', handleMouseUp);
scrollContainer.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<div
ref={containerRef}
className={`relative inline-flex bg-gray-300/80 rounded-full p-1 dark:bg-gray-700 ${
className={`relative inline-flex bg-gray-300/80 rounded-full p-1 dark:bg-gray-700 max-w-full ${
className || ''
}`}
>
{/* 滑动的白色背景指示器 */}
{indicatorStyle.width > 0 && (
<div
className='absolute top-1 bottom-1 bg-white dark:bg-gray-500 rounded-full shadow-sm transition-all duration-300 ease-out'
style={{
left: `${indicatorStyle.left}px`,
width: `${indicatorStyle.width}px`,
}}
/>
)}
{options.map((opt, index) => {
const isActive = active === opt.value;
return (
<button
key={opt.value}
ref={(el) => {
buttonRefs.current[index] = el;
{/* 可滚动容器 */}
<div
ref={scrollContainerRef}
className='relative flex overflow-x-auto scrollbar-hide'
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{/* 滑动的白色背景指示器 */}
{indicatorStyle.width > 0 && (
<div
className='absolute top-0 bottom-0 bg-white dark:bg-gray-500 rounded-full shadow-sm transition-all duration-300 ease-out pointer-events-none'
style={{
left: `${indicatorStyle.left}px`,
width: `${indicatorStyle.width}px`,
}}
onClick={() => onChange(opt.value)}
className={`relative z-10 flex items-center justify-center gap-1.5 px-3 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm rounded-full font-medium transition-all duration-200 cursor-pointer ${
isActive
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
}`}
>
{opt.icon && <span className='inline-flex items-center'>{opt.icon}</span>}
{opt.label}
</button>
);
})}
/>
)}
{options.map((opt, index) => {
const isActive = active === opt.value;
return (
<button
key={opt.value}
ref={(el) => {
buttonRefs.current[index] = el;
}}
onClick={(e) => {
// 如果正在拖动,阻止点击
if (hasDraggedRef.current) {
e.preventDefault();
return;
}
onChange(opt.value);
}}
className={`relative z-10 flex items-center justify-center gap-1.5 px-3 py-1 text-xs sm:px-4 sm:py-2 sm:text-sm rounded-full font-medium transition-all duration-200 cursor-pointer whitespace-nowrap flex-shrink-0 ${
isActive
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'
}`}
>
{opt.icon && <span className='inline-flex items-center'>{opt.icon}</span>}
{opt.label}
</button>
);
})}
</div>
</div>
);
};

View File

@@ -40,7 +40,7 @@ export interface VideoCardProps {
source_names?: string[];
progress?: number;
year?: string;
from: 'playrecord' | 'favorite' | 'search' | 'douban' | 'tmdb';
from: 'playrecord' | 'favorite' | 'search' | 'douban' | 'tmdb' | 'source-search';
currentEpisode?: number;
douban_id?: number;
tmdb_id?: number;
@@ -441,6 +441,16 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
showRating: !!rate,
showYear: false,
},
'source-search': {
showSourceName: false,
showProgress: false,
showPlayButton: true,
showHeart: true,
showCheckCircle: false,
showDoubanLink: true,
showRating: !!rate,
showYear: true,
},
};
return configs[from] || configs.search;
}, [from, isAggregate, douban_id, rate, isUpcoming]);