SwipeTabs 新增 swipeable 开关, Macro 开启, 打卡/监控保持原行为; 使用 swiper 14 实现整屏分页 + 首尾橡皮筋 + 松手吸附 + 方向锁; 滑动/点击/路由双向同步, 子 tab 切换用 replace 不堆历史; 每个 slide 独立竖向滚动, pane 节点缓存避免横滑时重渲染列表; 宏观内部切 tab 不再重复播放整页入场动画
This commit is contained in:
@@ -1,17 +1,25 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Tabs } from 'antd-mobile'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import 'swiper/css'
|
||||
|
||||
// 顶部分类 + 下方仅渲染当前激活 tab 的内容
|
||||
// 顶部分类 + 下方 tab 内容
|
||||
// - 顶部: antd-mobile Tabs (点击切换)
|
||||
// - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态)
|
||||
// - 下方: 同时挂载所有 tab, 保留各 tab 内部状态
|
||||
// - 右侧溢出提示: 当 tab 条还能向右滚动时, 右边缘显示渐变 + 箭头,
|
||||
// 提示"右侧还有更多 tab"; 滚动到末尾后自动消失
|
||||
// props:
|
||||
// tabs: [{ key, label, render }]
|
||||
// active: 当前激活 key
|
||||
// onChange(key): 切换回调
|
||||
export default function SwipeTabs({ tabs, active, onChange }) {
|
||||
// swipeable: 是否开启整屏左右滑动 (默认 false, 打卡/监控保持纯点击切换)
|
||||
// - true: 每个 tab 一屏, Swiper 提供首尾橡皮筋 + 松手吸附 + 方向锁
|
||||
// - false: 旧的 [hidden] 方案, 行为与改造前完全一致
|
||||
const SWIPE_MS = 260
|
||||
|
||||
export default function SwipeTabs({ tabs, active, onChange, swipeable = false }) {
|
||||
const wrapRef = useRef(null)
|
||||
const swiperRef = useRef(null)
|
||||
const [more, setMore] = useState(false)
|
||||
const [headerH, setHeaderH] = useState(45)
|
||||
|
||||
@@ -59,6 +67,29 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
||||
list.scrollTo({ left: Math.max(0, target), behavior: 'smooth' })
|
||||
}, [active, tabs.length])
|
||||
|
||||
// 外部原因改变 active (点击 tab / 前进后退 / 分类动态加载) 时, 让 Swiper 跟到对应整屏。
|
||||
// 手指滑动自身触发的 active 更新不会重复动画: 此时 swiper.activeIndex 已等于目标索引。
|
||||
useEffect(() => {
|
||||
if (!swipeable) return
|
||||
const sw = swiperRef.current
|
||||
if (!sw) return
|
||||
const idx = tabs.findIndex((t) => t.key === active)
|
||||
if (idx < 0 || sw.activeIndex === idx) return
|
||||
sw.slideTo(idx, SWIPE_MS)
|
||||
}, [swipeable, active, tabs])
|
||||
|
||||
const activeIndex = useMemo(() => {
|
||||
const idx = tabs.findIndex((t) => t.key === active)
|
||||
return idx < 0 ? 0 : idx
|
||||
}, [tabs, active])
|
||||
|
||||
// 所有 pane 的 React 节点按 tabs 缓存: 切页 (active 变化) 时不重新渲染各 tab 内容,
|
||||
// 既保留内部状态, 也避免横滑过程中多个列表一起重渲染造成掉帧。
|
||||
const panes = useMemo(
|
||||
() => (swipeable ? tabs.map((t) => ({ key: t.key, node: t.render() })) : []),
|
||||
[swipeable, tabs],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="swipe-tabs-wrap" ref={wrapRef}>
|
||||
<Tabs activeKey={active} onChange={onChange}>
|
||||
@@ -78,13 +109,42 @@ export default function SwipeTabs({ tabs, active, onChange }) {
|
||||
<span className="tab-more-arrow">›</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="tab-content">
|
||||
{tabs.map((t) => (
|
||||
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
|
||||
{t.render()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{swipeable ? (
|
||||
// 整屏横滑: 首尾橡皮筋 (resistance) + 松手吸附, 竖向滚动仍交给浏览器
|
||||
<Swiper
|
||||
className="swipe-tabs-swiper"
|
||||
initialSlide={activeIndex}
|
||||
slidesPerView={1}
|
||||
spaceBetween={0}
|
||||
speed={SWIPE_MS}
|
||||
threshold={6}
|
||||
resistance
|
||||
resistanceRatio={0.65}
|
||||
followFinger
|
||||
longSwipesRatio={0.35}
|
||||
longSwipesMs={250}
|
||||
shortSwipes
|
||||
touchAngle={45}
|
||||
roundLengths
|
||||
onSwiper={(sw) => { swiperRef.current = sw }}
|
||||
onSlideChange={(sw) => {
|
||||
const t = tabs[sw.activeIndex]
|
||||
if (t && t.key !== active) onChange(t.key)
|
||||
}}
|
||||
>
|
||||
{panes.map((p) => (
|
||||
<SwiperSlide key={p.key}>{p.node}</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
<div className="tab-content">
|
||||
{tabs.map((t) => (
|
||||
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
|
||||
{t.render()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user