import { useCallback, useEffect, useRef, useState } from 'react' import { Tabs } from 'antd-mobile' // 顶部分类 + 下方仅渲染当前激活 tab 的内容 // - 顶部: antd-mobile Tabs (点击切换) // - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态) // - 右侧溢出提示: 当 tab 条还能向右滚动时, 右边缘显示渐变 + 箭头, // 提示"右侧还有更多 tab"; 滚动到末尾后自动消失 // props: // tabs: [{ key, label, render }] // active: 当前激活 key // onChange(key): 切换回调 export default function SwipeTabs({ tabs, active, onChange }) { const wrapRef = useRef(null) const [more, setMore] = useState(false) const [headerH, setHeaderH] = useState(45) // 判断右侧是否还有未展示的 tab (顺带记录 tab 条高度, 让提示层只覆盖页签行) const measure = useCallback(() => { const wrap = wrapRef.current if (!wrap) return const list = wrap.querySelector('.adm-tabs-tab-list') const header = wrap.querySelector('.adm-tabs-header') if (header && header.offsetHeight > 0) setHeaderH(header.offsetHeight) if (!list) return setMore(list.scrollLeft + list.clientWidth < list.scrollWidth - 2) }, []) useEffect(() => { const wrap = wrapRef.current if (!wrap) return const list = wrap.querySelector('.adm-tabs-tab-list') const header = wrap.querySelector('.adm-tabs-header') if (!list) return // 首次布局未稳定 (字体/宽度), 下一帧 + 300ms 各补测一次 const raf = requestAnimationFrame(measure) const timer = setTimeout(measure, 300) list.addEventListener('scroll', measure, { passive: true }) const ro = new ResizeObserver(measure) ro.observe(list) if (header) ro.observe(header) return () => { cancelAnimationFrame(raf) clearTimeout(timer) list.removeEventListener('scroll', measure) ro.disconnect() } }, [measure, tabs.length]) // 切换 tab 后把激活项滚到可见区域中间, 避免选中项被藏在屏幕外 useEffect(() => { const wrap = wrapRef.current if (!wrap) return const list = wrap.querySelector('.adm-tabs-tab-list') const activeEl = wrap.querySelector('.adm-tabs-tab-active') if (!list || !activeEl) return const target = activeEl.offsetLeft - (list.clientWidth - activeEl.offsetWidth) / 2 list.scrollTo({ left: Math.max(0, target), behavior: 'smooth' }) }, [active, tabs.length]) return (