feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useState,
useRef,
useEffect,
type Dispatch,
type SetStateAction,
} from 'react';
import {
useInfiniteScroll,
useUpdateEffect,
useMemoizedFn,
useDebounceFn,
} from 'ahooks';
import { type ScrollProps, type InfiniteListDataProps } from '../type';
/* 滚动Hooks */
function useForwardFunc<T>(
dataInfo: InfiniteListDataProps<T>,
mutate: Dispatch<SetStateAction<InfiniteListDataProps<T>>>,
) {
// 手动插入数据,不通过接口
const insertData = (item, index) => {
dataInfo.list.splice(index, 0, item);
mutate({
...dataInfo,
list: [...(dataInfo?.list || [])],
});
};
// 手动删除数据,不通过接口
const removeData = index => {
dataInfo.list.splice(index, 1);
mutate({
...dataInfo,
list: [...(dataInfo?.list || [])],
});
};
const getDataList = () => dataInfo?.list;
return { insertData, removeData, getDataList };
}
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- 看了下代码行数不太好优化
function useScroll<T>(props: ScrollProps<T>) {
const {
targetRef,
loadData,
threshold,
reloadDeps,
isNeedBtnLoadMore,
resetDataIfReload = true,
} = props;
const [isLoadingError, setIsLoadingError] = useState<boolean>(false);
const refFetchNo = useRef<number>(0);
const refResolve = useRef<(value) => void>();
const {
loading,
data: dataInfo,
loadingMore,
loadMore,
noMore,
cancel,
mutate,
reload,
} = useInfiniteScroll<InfiniteListDataProps<T>>(
async current => {
// 此处逻辑如此复杂是解决Scroll中的bug。
// useInfiniteScroll中的cancel只是取消了一次请求但是数据会根据current重新设置一遍。
const fetchNo = refFetchNo.current;
if (refResolve.current) {
// 保证顺序执行,如果有当前方法,就取消上一次的请求,防止出现由于网络原因导致数据覆盖问题
// 同时发出A1,A2,三次请求但是A1先到达然后请求了B1, 但是A1过慢导致了A1覆盖了B1的请求。
refResolve.current({
...(current || {}),
list: [],
});
}
const result = await new Promise((resolve, reject) => {
refResolve.current = resolve;
loadData(current)
.then(value => resolve(value))
.catch(err => reject(err));
});
// @ts-expect-error -- linter-disable-autofix
refResolve.current = null;
// 切换Tab的时候如果此时正在请求防止数据的残留界面显示
if (refFetchNo.current !== fetchNo) {
if (current) {
current.list = [];
}
return {
list: [],
nextPage: 1,
};
}
return result as InfiniteListDataProps<T>;
},
{
target: isLoadingError || isNeedBtnLoadMore ? null : targetRef, //失败的时候通过去掉target的事件绑定禁止滚动加载。
threshold,
onBefore: () => {
//setIsLoadingError(false);
},
isNoMore: data => data?.hasMore !== undefined && !data?.hasMore,
onSuccess: () => {
if (isLoadingError) {
setIsLoadingError(false);
}
},
onError: e => {
// 如果在请求第一页数据时发生错误并且当前列表不为空则reset数据
// 这个case只有当resetDataIfReload设置为false时才会发生
// @ts-expect-error -- linter-disable-autofix
if (dataInfo.nextPage === 1 && (dataInfo?.list?.length ?? 0) > 0) {
// @ts-expect-error -- linter-disable-autofix
mutate({
...dataInfo,
list: [],
});
}
setIsLoadingError(true);
},
},
);
const { insertData, removeData, getDataList } = useForwardFunc(
// @ts-expect-error -- linter-disable-autofix
dataInfo,
mutate,
);
useEffect(() => {
if (isNeedBtnLoadMore && !(loading || loadingMore)) {
reload();
}
}, []);
const reloadData = useMemoizedFn(() => {
mutate({
list: resetDataIfReload ? [] : (dataInfo?.list ?? []),
hasMore: undefined,
nextPage: 1,
});
cancel();
setIsLoadingError(false);
reload();
});
useUpdateEffect(() => {
refFetchNo.current++;
reloadData();
}, [...(reloadDeps || [])]);
const isLoading = loading || loadingMore || props.isLoading;
const { run: loadMoreDebounce } = useDebounceFn(
() => {
if (isLoading) {
return;
}
if (!isNeedBtnLoadMore) {
loadMore();
}
},
{ wait: 500 },
);
useEffect(() => {
const resize = () => {
loadMoreDebounce();
};
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, []);
const { list } = dataInfo || {};
return {
dataList: list,
isLoading,
loadMore: () => {
if (!isLoading) {
//如果已经有数据加载中了,需要禁止重复加载。
loadMore();
}
},
reload: reloadData,
noMore,
cancel,
isLoadingError,
mutate,
insertData,
removeData,
getDataList,
};
}
export default useScroll;