记录自用的上拉刷新,下拉加载组件 (仅供学习)
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 = {
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);
}, [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;