From 30b512e5549f58eb6f52aebc33e7918f41b224ae Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 7 Apr 2026 12:22:31 +0800 Subject: [PATCH] fix(web): DebounceSelect support page load --- web/src/components/DebounceSelect/index.tsx | 98 +++++++++++++-------- web/src/components/Header/index.tsx | 4 +- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/web/src/components/DebounceSelect/index.tsx b/web/src/components/DebounceSelect/index.tsx index ab8379ad..9121b30d 100644 --- a/web/src/components/DebounceSelect/index.tsx +++ b/web/src/components/DebounceSelect/index.tsx @@ -10,6 +10,7 @@ interface OptionType { interface ApiResponse { items?: T[]; + page: { hasnext: boolean }; } export interface DebounceSelectProps extends Omit { @@ -23,8 +24,9 @@ export interface DebounceSelectProps extends Omit { labelKey?: string; /** Key name sent to the API for the search keyword */ searchKey?: string; + pageSize?: number; /** Custom fetch function — mutually exclusive with url */ - fetchOptions?: (search: string | null) => Promise; + fetchOptions?: (search: string | null, page: number) => Promise<{ options: DefaultOptionType[]; hasMore: boolean }>; /** Transform raw API items before rendering */ format?: (items: OptionType[]) => OptionType[]; debounceTimeout?: number; @@ -32,10 +34,11 @@ export interface DebounceSelectProps extends Omit { const DebounceSelect: FC = ({ url, - params = { page: 1, pagesize: 20 }, + params = {}, valueKey = 'value', labelKey = 'label', searchKey = 'search', + pageSize = 20, fetchOptions, format, debounceTimeout = 300, @@ -43,56 +46,81 @@ const DebounceSelect: FC = ({ }) => { const [fetching, setFetching] = useState(false); const [options, setOptions] = useState([]); + const [hasMore, setHasMore] = useState(true); + const pageRef = useRef(1); + const keywordRef = useRef(null); const fetchRef = useRef(0); - const timerRef = useRef>(); - // Load initial options on mount - useEffect(() => { - debounceFetcher(null); - }, []); + const fetchPage = useCallback((keyword: string | null, page: number, replace: boolean) => { + fetchRef.current += 1; + const fetchId = fetchRef.current; + setFetching(true); + + const promise = fetchOptions + ? fetchOptions(keyword, page) + : request + .get>(url!, { ...params, [searchKey]: keyword, page, pagesize: pageSize }) + .then((res) => { + const data: OptionType[] = Array.isArray(res) ? res : res?.items || []; + const formatted = format + ? format(data) + : data.map((item) => ({ label: item[labelKey], value: item[valueKey], avatar: item.avatar, raw: item })); + + console.log('more', res.page?.hasnext) + return { options: formatted, hasMore: res.page?.hasnext }; + }); + + promise + .then(({ options: newOptions, hasMore: more }) => { + if (fetchId !== fetchRef.current) return; + setOptions((prev) => (replace ? newOptions : [...prev, ...newOptions])); + setHasMore(more); + setFetching(false); + }) + .catch(() => setFetching(false)); + }, [url, params, searchKey, fetchOptions, format, valueKey, labelKey, pageSize]); const debounceFetcher = useCallback((keyword: string | null) => { clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { - fetchRef.current += 1; - const fetchId = fetchRef.current; - setOptions([]); - setFetching(true); - - const promise: Promise = fetchOptions - ? fetchOptions(keyword) - : request - .get>(url!, { ...params, [searchKey]: keyword }) - .then((res) => { - const data: OptionType[] = Array.isArray(res) ? res : res?.items || []; - const formatted = format ? format(data) : data.map((item) => ({ - label: item[labelKey], - value: item[valueKey], - avatar: item.avatar, - raw: item, - })); - return formatted; - }); - - promise - .then((newOptions) => { - if (fetchId !== fetchRef.current) return; - setOptions(newOptions); - setFetching(false); - }) - .catch(() => setFetching(false)); + keywordRef.current = keyword; + pageRef.current = 1; + fetchPage(keyword, 1, true); }, debounceTimeout); - }, [url, params, searchKey, fetchOptions, format, valueKey, labelKey, debounceTimeout]); + }, [fetchPage, debounceTimeout]); + + useEffect(() => { + debounceFetcher(null); + }, []); + + const handlePopupScroll = useCallback((e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (!fetching && hasMore && scrollHeight - scrollTop - clientHeight < 50) { + const nextPage = pageRef.current + 1; + pageRef.current = nextPage; + fetchPage(keywordRef.current, nextPage, false); + } + }, [fetching, hasMore, fetchPage]); return (