【react】InfiniteScroll 滚动组件

程景胜
2023-12-01

在data.d.ts中定义父组件所需要传的值

import { ReactNode } from 'react'
type PullStatus = 'pulling' | 'canRelease' | 'refreshing' | 'complete'
interface InfiniteProps {
  isNavBar?: boolean // 是否有头部
  TopNoData?: number // 无数据的时候图片距离顶部的距离
  InitialNoData?: {
    img: string // 初始进入无数据的图片
    content: ReactNode | string // 内容区域 可以是文本或者reactNode
  } // 初始进入无数据传入的
  searchNoData?: {
    img: string // 搜索无数据的img
    content: ReactNode | string // 内容区域 可以是文本或者reactNode
  } // 搜索无数据的
  scrollMap: {
    isNull?: boolean
    lastPage: boolean | undefined
    isLoading: boolean | undefined
    setSize: (num: number) => void
    size: number | undefined
    refresh: (obj?: object) => void
  } // 传入的滚动组件所需要的值, 这是一个map调用request的时候返回的scrollMap
  skeletionData?: string // 传入的列表骨架屏图片
  TopDistance?: number // 滚动组件距离顶部的距离
  isBottomPadding?: boolean //是否给底部没有数据提示加padding
  pageSize?: number // 接受传入的每页的数据量
  data: Array<[]> // 接收传入的每页的数据
  height?: number | string // 滚动组件高度 可以传入可以不传
  backgroundColor?: string | '#fff' // 传入的背景颜色
  isScrollRecovery?: boolean // 是否执行滚动位置恢复
  childrenContent?: ReactNode // 内容
  // eslint-disable-next-line @typescript-eslint/ban-types
  handlerRefresh?: () => void // 触发刷新时的处理函数(可以不用)
  pullingText?: ReactNode // 下拉的提示文案
  canReleaseText?: ReactNode // 释放的提示文案
  refreshingText?: ReactNode // 刷新时的提示文案
  completeText?: ReactNode // 完成时的提示文案
  completeDelay?: number // 完成后延迟消失的时间,单位为 ms
  headHeight?: number // 头部提示内容区的高度,单位为 px
  threshold?: number // 触发刷新需要下拉多少距离,单位为 px
  dropText?: (status: PullStatus) => ReactNode // 根据下拉状态,自定义下拉提示文案
}
export { InfiniteProps }

在index.tsx中实现此功能

import React, { useRef, useEffect } from 'react'
import { Main } from './styled'
import { InfiniteProps } from './data'
import { Loading, PullToRefresh, Image } from 'antd-mobile'
import { useLocation } from 'react-router-dom'
import { useInViewport, useSafeState, useUpdateEffect } from 'ahooks'
import { chunk } from 'lodash-es'
// 列表进入详情,从详情返回到列表保留滚动条位置
import { useScrollToRestore } from '@/hooks/scrollToRestore'
import EmptyState from '../EmptyState' // 空状态组件引入

const InfiniteScroll: React.FC<InfiniteProps> = (props) => {
  const {
  height = 0,
    data,
    pageSize = 15,
    backgroundColor='#ffffff',
    isNavBar = false,
    scrollMap,
    skeletionData = '',
    TopDistance,
    isBottomPadding= false,
    childrenContent,
    isScrollRecovery= true,
    handlerRefresh,
    pullingText = '下拉刷新',
    canReleaseText = '释放立即刷新',
    refreshingText = '加载中……',
    completeText = '刷新成功',
    completeDelay = 500,
    headHeight = 40,
    threshold = 60,
    TopNoData, // 无数据的时候图片距离顶部的距离
    InitialNoData, // 初始进入无数据的状态
    searchNoData, // 搜索无数据的状态
  } = props

  const {
    refresh,
    lastPage,
    size = 0,
    isLoading,
    setSize,
    isNull,
  } = scrollMap
  // 判断是否是显示加载中
  const [isDisplayLoad, setIsDisplayLoad] = useSafeState(false)
  // 判断是否是最后一条数据
  const [isLastData, setIsLastData] = useSafeState(false)
  // 加载中的ref
  const loadingRef = useRef<HTMLDivElement>(null)
  // 监听加载中是否显示到可视窗口
  const [inViewport] = useInViewport(loadingRef)
  // 获取当前路由地址
  const { pathname } = useLocation()
  // 是否显示出来加载中 如果是true 则加载下一页
  useUpdateEffect(() => {
    if (inViewport && setSize) {
      setSize(size + 1)
    }
  }, [inViewport])
  // 设置是否显示加载中以及判断是否是最后一页
  useEffect(() => {
     const displayLoad = data?.length === 0
     const initData = chunk(data, pageSize)
     const lastData = displayLoad || (initData && initData[initData.length - 1]?.length < pageSize)
     setIsLastData(!dataLastPage || lastData )
     setIsDisplayLoad(displayLoad)
  }, [data, dataLastPage])
  // 调用滚动位置恢复hooks
  useScrollToRestore(isScroll, 'roolDom')
  // 底部渲染
  const handlerBottom = () => {
    let content = ''
    if (isDisplayLoad) {
      content = ''
    } else if (!isLastData && isDisplayLoad) {
      content = ''
    } else {
      content = '没有更多了~'
    }
    return content
  }
  return (
    <>
      <InfiniteMain
        isNavBar={isNavBar || import.meta.env.MODE.includes('lcoal') }
        TopDistance={TopDistance}
        isAddPadding={isBottomPadding}
        dataNall={data.length === 0}
        className="roolDom"
        height={height}
        backgroundColor={backgroundColor}
      >
        {!isLoading ? (
          <>
            {/* 初始进来页面无数据 */}
            {InitialNoData && !isNull && data.length === 0 && (
              <EmptyState
                isNavBar={isNavBar}
                defaultImg={InitialNoData?.img}
                top={top || 0}
                title={InitialNoData?.content|| ''}
              />
            )}
            {/* 页面有数据 搜索无数据 */}
            {searchNoData && isNull && data.length === 0 && (
              <EmptyState
                isNavBar={isNavBar}
                defaultImg={searchNoData?.img}
                top={TopNoData || 0}
                title={searchNoData?.content || ''}
              />
            )}
            <PullToRefresh
              onRefresh={async () => {
                await (handlerRefresh && handlerRefresh())
                refresh && refresh()
                sessionStorage.removeItem(pathname)
              }}
              pullingText={pullingText}
              canReleaseText={canReleaseText}
              refreshingText={refreshingText}
              completeText={completeText}
              completeDelay={completeDelay}
              headHeight={headHeight}
              threshold={threshold}
            >
              <div className="mainBox">{childrenContent}</div>
              {!isLastData ? (
                <div ref={isLoadingRef} className="base_load">
                  <div>加载中</div>
                  <Loading />
                </div>
              ) : (
                handlerBottom() && <div className="botm">{handlerBottom()}</div>
              )}
            </PullToRefresh>
          </>
        ) : (
          skeletionData && <Image src={skeletionData} />
        )}
      </InfiniteMain>
    </>
  )
}
export default React.memo(InfiniteScroll)

在styled.ts中实现样式

import styled from 'ns-styled'
const InfiniteMain = styled.div<{
  TopDistance?: number
  isAddPadding?: boolean
  height: number | string
  backgroundColor?: string | '#fff'
  dataNall: boolean
  isNavBar: boolean
}>`
  background: ${(p) => p.dataNall && '#f5f5f5'};
  &::-webkit-scrollbar {
    display: none;
  }
  .adm-pull-to-refresh-head-content {
    font-size: 14px;
  }
  .base_load {
    height: 36px;
    background: #f7f7f7;
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    div {
      display: inline-block;
    }
  }
  .botm {
    font-size: 12px;
    color: #8c8c8c;
    text-align: center;
    height: 24px;
    line-height: 24px;
    margin-top: 32px;
    margin-bottom: ${(props) => props.isAddPadding && '56px'};
  }
  height: ${(p) =>
    p.TopDistance
      ? `calc(100vh - ${(p.TopDistance || 0) + (p.isNavBar ? 52 : 0)}px)`
      : Number(p.height)
      ? `${p.height}px`
      : p.height};
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  background: ${(props) => props.backgroundColor};
`
export { InfiniteMain }
 类似资料: