记录自用的上拉刷新,下拉加载组件(仅供学习)

记录自用的上拉刷新,下拉加载组件 (仅供学习)

在这里插入图片描述

base.tsx

import { InfiniteScroll, PullToRefresh } from '@ali/ic-base-antd-mobile';
import classnames from 'classnames';
import { useMemo, type FC, type ReactNode } from 'react';
import styles from './index.module.less';

export type ReloadType = (resetPageIndex?: boolean) => Promise<void>;
export interface IScroll<DataSource> {
  /** 下拉刷新回调 */
  reload: ReloadType;
  /** 加载数据时是否出错 */
  hasError?: boolean;
  /** 报错页面 */
  errorPage?: ReactNode;
  /** 空页面 */
  emptyPage?: ReactNode;
  /** 没有更多 */
  noMore?: boolean;
  /** 触底刷新的触发方法 */
  onLoad: () => Promise<any>;
  data: DataSource[];
  className?: string;
  /** 子项 */
  RenderItem?: FC<DataSource & ActionType>;
  /** 渲染所有项 */
  renders?: (data: DataSource[], action: ActionType) => ReactNode;
  /** 数据是否已获取 */
  hasGetData: boolean;
}

export type ActionType = {
  /** @name 刷新 */
  reload: ReloadType;
};

function ScrollBase<DataSource = object>({
  hasError,
  errorPage,
  noMore,
  onLoad,
  data,
  className,
  RenderItem,
  renders,
  reload,
  emptyPage,
  hasGetData,
}: IScroll<DataSource>) {
  const hasData = !!data?.length;

  const loadMore = async () => {
    await onLoad();
  };

  const dataNode = useMemo(() => {
    if (hasData) {
      if (renders) {
        return renders(data, { reload });
      }
      if (RenderItem) {
        return data.map((item, index) => (
          <RenderItem key={JSON.stringify(item)} {...item} reload={reload} />
        ));
      }
    }

    return null;
  }, [hasData, renders, RenderItem, data, reload]);

  if (hasError && errorPage) {
    return errorPage;
  }

  if (hasGetData && !dataNode) {
    return emptyPage;
  }
  return (
    <div id="scroll" className={classnames(styles.scrollBox, className)}>
      {/* 下拉刷新 */}
      <PullToRefresh onRefresh={reload}>
        {/* 数据 */}
        <div>{dataNode}</div>

        {/* 上拉加载 */}
        <InfiniteScroll loadMore={loadMore} hasMore={!noMore} />
      </PullToRefresh>
    </div>
  );
}

export default ScrollBase;

scrollListFetch.tsx

import useSyncState from '@/utils/hooks/useSyncState';
import type { ICRequest } from '@ali/ic-base-request';
import { useUpdateEffect } from 'ahooks';
import classnames from 'classnames';
import { useCallback, useEffect, useImperativeHandle, useState } from 'react';
import ErrorPage from '../errorPage';
import { ErrorImgEnum } from '../errorPage/constants';
import ScrollBase, { type ActionType, type IScroll } from './base';

interface IScrollListFetch<DataSource, P>
  extends Partial<Omit<IScroll<DataSource>, 'onLoad'>> {
  params?: P;
  request: (
    arg: P & ICRequest.PaginationParam,
  ) => Promise<ICRequest.PaginationResult<DataSource>>;
  pagination?: ICRequest.PaginationParam;
  actionRef?: React.Ref<ActionType | undefined>;
  onDataSourceChange?: (dataSource: DataSource[]) => void;
  onSuccessFetch?: (res: ICRequest.PaginationResult<DataSource>) => void;
}

interface IHandleSuccessFetch<DataSource> {
  res: ICRequest.PaginationResult<DataSource>;
  pageNum: number;
}

let rId = 0;
const defPagination: ICRequest.PaginationParam = {
  currentPage: 1,
  pageSize: 200,
};

const defErrorPage = (
  <ErrorPage img={ErrorImgEnum['无数据']} title="暂无数据" />
);

function ScrollListFetch<DataSource = object, P = object>({
  className,
  params,
  pagination = defPagination,
  request,
  actionRef: propsActionRef,
  errorPage = defErrorPage,
  onDataSourceChange,
  onSuccessFetch,
  ...scrollProps
}: IScrollListFetch<DataSource, P>) {
  const [data, setData] = useSyncState<DataSource[]>([]);

  const [currentPage, setCurrentPage] = useState(0);
  const [hasError, setHasError] = useState(false);
  const [noMore, setNoMore] = useState(false);
  const [hasGetData, setHasGetData] = useState(false);

  // 请求成功后
  const handleSuccessFetch = useCallback(
    ({ res, pageNum }: IHandleSuccessFetch<DataSource>) => {
      setHasGetData(true);
      onSuccessFetch?.(res);
      setCurrentPage(pageNum);

      if (res.totalCount === 0) {
        setNoMore(true);
      }
      setData(
        (pre) => {
          const items = res.items ?? [];
          return pageNum === 1 ? [...items] : [...pre, ...items];
        },
        (pre) => {
          if (res.totalCount <= pre.length) {
            setNoMore(true);
          } else {
            setNoMore(false);
          }
        },
      );
    },
    [onSuccessFetch, setData],
  );

  const fetchData = useCallback(
    (pageNum = currentPage) => {
      const currentRId = ++rId;
      return request({ ...params, ...pagination, currentPage: pageNum } as P &
        ICRequest.PaginationParam)
        .then((res) => {
          if (currentRId !== rId) {
            return;
          }
          setHasError(false);
          handleSuccessFetch({ res, pageNum });
          return res.items;
        })
        .catch(() => {
          setHasError(true);
        });
    },
    [currentPage, handleSuccessFetch, pagination, params, request],
  );

  /** 上拉加载 */
  const onLoad = useCallback(async () => {
    const pageNum = currentPage + 1;
    await fetchData(pageNum);
  }, [currentPage, fetchData]);

  /** 刷新 */
  const reload = useCallback(
    async (resetPageIndex?: boolean) => {
      const pageNum = resetPageIndex ? 1 : currentPage;
      await fetchData(pageNum);
    },
    [currentPage, fetchData],
  );

  useImperativeHandle(
    propsActionRef,
    () => ({
      reload,
    }),
    [reload],
  );

  useUpdateEffect(() => {
    reload(true);
  }, [JSON.stringify(params)]);

  useEffect(() => {
    onDataSourceChange?.(data);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data]);

  return (
    <ScrollBase<DataSource>
      className={classnames([className])}
      data={data}
      noMore={noMore}
      reload={reload}
      hasError={hasError}
      onLoad={onLoad}
      errorPage={errorPage}
      emptyPage={defErrorPage}
      hasGetData={hasGetData}
      {...scrollProps}
    />
  );
}

export default ScrollListFetch;

useScrollBottomOut.ts

import { useDebounceFn, useMemoizedFn } from 'ahooks';
import { useEffect, useState } from 'react';

/** 上拉触底 */
const useScrollBottomOut = ({
  onChange,
  noMore,
  loading,
}: IScrollBottomOutParams): { currentPage: number } => {
  const [currentPage, setCurrentPage] = useState(1);
  const doc = document.documentElement;

  const { clientHeight } = doc;
  const distance = 150;

  // 上拉触底
  const scrollBottomingOut = useMemoizedFn(() => {
    const scrollTop =
      document.documentElement.scrollTop ||
      window.pageYOffset ||
      document.body.scrollTop;
    const { scrollHeight } = doc;
    const isOut = clientHeight + scrollTop >= scrollHeight - distance; // 触底

    if (isOut && !noMore && onChange && !loading) {
      setCurrentPage((pre) => pre + 1);
      onChange(currentPage);
    }
  });

  const { run: onScrollBottomingOut } = useDebounceFn(scrollBottomingOut, {
    wait: 50,
  });
  useEffect(() => {
    document.addEventListener('scroll', onScrollBottomingOut);
    return () => {
      document.removeEventListener('scroll', onScrollBottomingOut);
    };
  }, [onScrollBottomingOut]);
  return { currentPage };
};

export default useScrollBottomOut;
interface IScrollBottomOutParams {
  onChange?: (currentPage: number) => void;
  noMore?: boolean;
  loading?: boolean;
}

css

.LoadMore {
  margin-top: 6px;
  font-size: 12px;
  height: 27px;
  line-height: 27px;
  color: #999;
  text-align: center;
  background-color: transparent !important;
}

.scrollBox {
  height: auto;

  .noData {
    margin-top: 50%;
    margin-left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;

    img {
      height: 160px;
      width: 160px;
    }
  }
}

index.tsx

import ScrollList from './scrollListFetch';

export default ScrollList;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值