150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
import { useRef, useEffect, useState, useCallback } from 'react';
|
||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||
import styles from './index.module.css';
|
||
|
||
type Data = {
|
||
tabItems: {
|
||
icon?: string;
|
||
tabName?: string;
|
||
contentTitle?: string;
|
||
contentSubtitle?: string;
|
||
contentText?: string;
|
||
content?: string;
|
||
/** 以 mockData 为准,tabItems 可能使用 sideImage 或 backgroundImage */
|
||
sideImage?: string;
|
||
path?: string;
|
||
}[],
|
||
backgroundImage?: string;
|
||
titleDirection?: 'row' | 'column';
|
||
className?: string;
|
||
}
|
||
export default function TopTabs({ data, activeIndex, setActiveIndex, className }: { data: Data, activeIndex: number, setActiveIndex: (index: number) => void, className?: string }) {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const scrollRef = useRef<HTMLDivElement>(null);
|
||
|
||
const [indicatorStyle, setIndicatorStyle] = useState<{ left: number; width: number }>({ left: 0, width: 0 });
|
||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||
const [shouldCenter, setShouldCenter] = useState(true);
|
||
|
||
const updateIndicatorPosition = useCallback(() => {
|
||
const scrollEl = scrollRef.current;
|
||
const activeTab = activeIndex < data.tabItems.length
|
||
? (scrollEl?.children[activeIndex] as HTMLElement)
|
||
: null;
|
||
|
||
if (!scrollEl || !activeTab) return;
|
||
|
||
const container = containerRef.current;
|
||
if (!container) return;
|
||
|
||
const containerRect = container.getBoundingClientRect();
|
||
const scrollRect = scrollEl.getBoundingClientRect();
|
||
const tabRect = activeTab.getBoundingClientRect();
|
||
|
||
const scrollLeft = scrollEl.scrollLeft;
|
||
const tabOffsetLeft = activeTab.offsetLeft;
|
||
|
||
const left = scrollRect.left - containerRect.left + tabOffsetLeft - scrollLeft;
|
||
const width = tabRect.width;
|
||
|
||
setIndicatorStyle({ left, width });
|
||
}, [activeIndex, data.tabItems.length]);
|
||
|
||
const updateScrollState = useCallback(() => {
|
||
const scrollEl = scrollRef.current;
|
||
if (!scrollEl) return;
|
||
const { scrollLeft, clientWidth, scrollWidth } = scrollEl;
|
||
setCanScrollLeft(scrollLeft > 0);
|
||
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
|
||
setShouldCenter(scrollWidth <= clientWidth);
|
||
}, []);
|
||
|
||
const handleScrollLeft = () => {
|
||
scrollRef.current?.scrollBy({ left: -200, behavior: 'smooth' });
|
||
};
|
||
|
||
const handleScrollRight = () => {
|
||
scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' });
|
||
};
|
||
|
||
useEffect(() => {
|
||
const scrollEl = scrollRef.current;
|
||
const handleScroll = () => {
|
||
updateIndicatorPosition();
|
||
updateScrollState();
|
||
};
|
||
|
||
const rafId = requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
updateIndicatorPosition();
|
||
updateScrollState();
|
||
});
|
||
});
|
||
|
||
if (!scrollEl) return () => cancelAnimationFrame(rafId);
|
||
|
||
scrollEl.addEventListener('scroll', handleScroll);
|
||
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
updateIndicatorPosition();
|
||
updateScrollState();
|
||
});
|
||
resizeObserver.observe(scrollEl);
|
||
|
||
return () => {
|
||
cancelAnimationFrame(rafId);
|
||
scrollEl.removeEventListener('scroll', handleScroll);
|
||
resizeObserver.disconnect();
|
||
};
|
||
}, [activeIndex, data.tabItems.length, updateIndicatorPosition, updateScrollState]);
|
||
|
||
return (
|
||
<div className={className}>
|
||
<div ref={containerRef} className={styles.topTabsTabs}>
|
||
{canScrollLeft && (
|
||
<button
|
||
type="button"
|
||
className={`${styles.topTabsNavBtn} ${styles.topTabsNavBtnLeft}`}
|
||
onClick={handleScrollLeft}
|
||
aria-label="向左滚动"
|
||
>
|
||
<LeftOutlined />
|
||
</button>
|
||
)}
|
||
<div
|
||
ref={scrollRef}
|
||
className={`${styles.topTabsTabsScroll} ${shouldCenter ? styles.topTabsTabsScrollCenter : ''}`}
|
||
>
|
||
{data.tabItems.map((item, index) => (
|
||
<div
|
||
key={index}
|
||
className={`${styles.topTabsTabItem} ${activeIndex === index ? styles.active : ''}`}
|
||
onClick={() => setActiveIndex(index)}
|
||
>
|
||
<span>{item.tabName}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{canScrollRight && (
|
||
<button
|
||
type="button"
|
||
className={`${styles.topTabsNavBtn} ${styles.topTabsNavBtnRight}`}
|
||
onClick={handleScrollRight}
|
||
aria-label="向右滚动"
|
||
>
|
||
<RightOutlined />
|
||
</button>
|
||
)}
|
||
<div
|
||
className={styles.topTabsBottomLine}
|
||
style={{
|
||
left: indicatorStyle.left,
|
||
width: indicatorStyle.width,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|