/* eslint-disable react-hooks/exhaustive-deps */ import React, { useEffect, useRef, useState } from 'react'; interface CapsuleSwitchProps { options: { label: string; value: string; icon?: React.ReactNode }[]; active: string; onChange: (value: string) => void; className?: string; } const CapsuleSwitch: React.FC = ({ options, active, onChange, className, }) => { const containerRef = useRef(null); const scrollContainerRef = useRef(null); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); const isScrollingRef = useRef(false); const scrollTimeoutRef = useRef(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; }>({ left: 0, width: 0 }); const activeIndex = options.findIndex((opt) => opt.value === active); // 更新指示器位置(仅更新位置,不触发滚动) const updateIndicatorPosition = (autoScroll = false) => { if ( activeIndex >= 0 && buttonRefs.current[activeIndex] && containerRef.current && scrollContainerRef.current ) { const button = buttonRefs.current[activeIndex]; const scrollContainer = scrollContainerRef.current; 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(true), 0); return () => clearTimeout(timeoutId); }, []); // 监听选中项变化,自动滚动到新选中项 useEffect(() => { 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 (
{/* 可滚动容器 */}
{/* 滑动的白色背景指示器 */} {indicatorStyle.width > 0 && (
)} {options.map((opt, index) => { const isActive = active === opt.value; return ( ); })}
); }; export default CapsuleSwitch;