react实现锚点定位(锚点滚动)

3,028 阅读1分钟

使用react实现的一个非常常见的需求,锚点定位(锚点滚动),实现和使用都非常的简单,我这边采用组件的方式实现,方便复用;代码如下请食用。

AnchorScroll组件(核心组件)

文件路径:/anchor-scroll/AnchorScroll.tsx

import { debounce } from '@/utils'
import { Tabs } from 'antd'
import React, { useEffect, useRef, useState } from 'react'
const { TabPane } = Tabs

type AnchorScrollType = {
    tabs: React.ReactNode[]
    initActiveKey?: String
    children: any
    className?: string
    height?: number // 滚动内容高度 (设置高度后就不能实现吸顶效果)
    style?: React.CSSProperties
    contentStyle?: React.CSSProperties
    tabBarStyle?: React.CSSProperties
    tabBarTop?: number // 距离顶部的距离
    offsetTop?: number // 滚动偏移量
    containerId?: string // 容器Id
    onChange?: Function
}
export default function AnchorScroll(props: AnchorScrollType) {
    let {
        tabs = [],
        initActiveKey = '0',
        children,
        className = '',
        tabBarStyle = {},
        style = {},
        contentStyle = {},
        height,
        tabBarTop = 0,
        offsetTop = 0,
        containerId,
        onChange,
    } = props
    const [activeKey, setActiveKey] = useState<any>(initActiveKey)
    const isClick = useRef(false) // 是否是点击触发

    contentStyle = height ? { ...contentStyle, height: height + 'px', overflow: 'auto' } : { ...contentStyle }

    // 初始化
    useEffect(() => {
        setActiveKey(initActiveKey)
        if (initActiveKey === '0') return
        onChangeTabs(initActiveKey)
    }, [initActiveKey])

    // 点击跳转到锚点
    const onChangeTabs = (e: any) => {
        onChange && onChange(e)
        setActiveKey(e)
        isClick.current = true
        const anchorElement = document.getElementById('anchor-scroll-item-' + e)
        if (anchorElement) {
            if (height) {
                anchorElement.scrollIntoView({ behavior: 'smooth' })
            } else {
                const scrollBox = document.querySelector('#' + containerId)
                scrollBox?.setAttribute('style', 'position: relative')
                scrollBox?.scrollTo(0, anchorElement.offsetTop - offsetTop)
            }
        }
    }

    // 根据滚动定位tab
    const onScroll = debounce(
        (e: any) => {
            let result: any[] = []
            tabs.forEach((item, index) => {
                const element = document.getElementById('anchor-scroll-item-' + index)
                result.push({ key: index.toString(), top: element?.getBoundingClientRect().top })
            })

            if (isClick.current) {
                isClick.current = false
                return
            }
            result.forEach((item, index) => {
                if (e.target.offsetTop >= item.top) {
                    setActiveKey(item.key)
                } else {
                    isClick.current = false
                }
            })
        },
        30,
        false
    )

    useEffect(() => {
        window.addEventListener('scroll', onScroll, true)
        return () => {
            const scrollBox = document.querySelector('#' + containerId)
            scrollBox?.scrollTo(0, 0)
            scrollBox?.setAttribute('style', 'position: static')
            window.removeEventListener('scroll', onScroll, true)
        }
    }, [])

    return (
        <div className={`anchor-scroll ${className}`} style={{ ...style }}>
            <Tabs
                tabBarStyle={{
                    padding: '0 24px',
                    ...tabBarStyle,
                }}
                activeKey={activeKey}
                destroyInactiveTabPane={true}
                className="tabs"
                style={{ top: tabBarTop + 'px' }}
                onChange={onChangeTabs}
            >
                {tabs.map((item: any, index: any) => {
                    return <TabPane tab={item} key={index}></TabPane>
                })}
            </Tabs>
            <div className="anchor-scroll-content" style={{ ...contentStyle }}>
                {
                    // 只渲染符合规格的子组件
                    findChild(children).map((item, index) => {
                        return React.cloneElement(item, { key: index, index: index })
                    })
                }
            </div>
        </div>
    )
}

// 查找子组件
function findChild(children: any[]): any[] {
    let result: any[] = []
    React.Children.forEach(children, (child, index) => {
        if (child?.type?.defaultProps?.name === 'AnchorScrollItem') {
            result.push(child)
        }
    })
    return result
}

AnchorScrollItem组件(组件目的就是绑定id)

文件路径:/anchor-scroll/AnchorScrollItem.tsx

import React from 'react'

export default function AnchorScrollItem(props: any) {
    return <div id={'anchor-scroll-item-' + props.index}>{props.children}</div>
}

使用方式

import AnchorScroll from '@/components/anchor-scroll/AnchorScroll'
import AnchorScrollItem from '@/components/anchor-scroll/AnchorScrollItem'
import React from 'react'

export default function Test(props: any) {
    return (
        <AnchorScroll initActiveKey="1" tabs={['tab1', 'tab2', 'tab3']}>
            <AnchorScrollItem>
                <div
                    style={{
                        width: '100%',
                        height: '500px',
                        border: '1px solid red',
                        marginBottom: '20px',
                    }}
                >
                    test1
                </div>
            </AnchorScrollItem>
            <AnchorScrollItem>
                <div
                    style={{
                        width: '100%',
                        height: '1000px',
                        border: '1px solid red',
                        marginBottom: '20px',
                    }}
                >
                    test2
                </div>
            </AnchorScrollItem>
            <AnchorScrollItem>
                <div
                    style={{
                        width: '100%',
                        height: '200px',
                        border: '1px solid red',
                    }}
                >
                    test3
                </div>
            </AnchorScrollItem>
        </AnchorScroll>
    )
}