update:添加分页、成员添加优化
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { CursorPage, CursorPageMeta } from "~/services/api";
|
||||
|
||||
export const ADMIN_PAGE_SIZE = 10;
|
||||
|
||||
type Fetcher<T> = (input: {
|
||||
limit: number;
|
||||
cursor: string | null;
|
||||
q: string;
|
||||
}) => Promise<CursorPage<T>>;
|
||||
|
||||
/**
|
||||
* Cursor-stack pagination for admin tables.
|
||||
* Supports prev/next and jumping to already-visited pages.
|
||||
* Changing ``q`` resets to page 1.
|
||||
*/
|
||||
export function useCursorPage<T>(fetcher: Fetcher<T>, q: string) {
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [meta, setMeta] = useState<CursorPageMeta>({
|
||||
limit: ADMIN_PAGE_SIZE,
|
||||
page_count: 0,
|
||||
total_count: 0,
|
||||
has_more: false,
|
||||
next_cursor: null,
|
||||
});
|
||||
// cursor used to *enter* page N. Page 1 is always null.
|
||||
const cursorByPageRef = useRef<Record<number, string | null>>({ 1: null });
|
||||
const nextCursorByPageRef = useRef<Record<number, string | null>>({});
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(meta.total_count / ADMIN_PAGE_SIZE));
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (targetPage: number, options?: { resetStack?: boolean }) => {
|
||||
const requestId = ++requestIdRef.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (options?.resetStack) {
|
||||
cursorByPageRef.current = { 1: null };
|
||||
nextCursorByPageRef.current = {};
|
||||
}
|
||||
const cursor = cursorByPageRef.current[targetPage] ?? null;
|
||||
if (targetPage > 1 && cursor === null && targetPage !== 1) {
|
||||
// Unvisited deep page — refuse rather than invent offset.
|
||||
return;
|
||||
}
|
||||
const result = await fetcher({
|
||||
limit: ADMIN_PAGE_SIZE,
|
||||
cursor: targetPage === 1 ? null : cursor,
|
||||
q,
|
||||
});
|
||||
if (requestId !== requestIdRef.current) return;
|
||||
setItems(result.items);
|
||||
setMeta(result.meta);
|
||||
setPage(targetPage);
|
||||
nextCursorByPageRef.current[targetPage] = result.meta.next_cursor;
|
||||
if (result.meta.next_cursor) {
|
||||
cursorByPageRef.current[targetPage + 1] = result.meta.next_cursor;
|
||||
}
|
||||
} catch {
|
||||
// Caller is responsible for notifying; keep previous items.
|
||||
} finally {
|
||||
if (requestId === requestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetcher, q],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadPage(1, { resetStack: true });
|
||||
}, [loadPage]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (!meta.has_more) return;
|
||||
void loadPage(page + 1);
|
||||
}, [loadPage, meta.has_more, page]);
|
||||
|
||||
const goPrev = useCallback(() => {
|
||||
if (page <= 1) return;
|
||||
void loadPage(page - 1);
|
||||
}, [loadPage, page]);
|
||||
|
||||
const goToPage = useCallback(
|
||||
(targetPage: number) => {
|
||||
if (targetPage < 1 || targetPage > totalPages) return;
|
||||
if (targetPage === page) return;
|
||||
// Only allow visited pages (cursor known) or page 1.
|
||||
if (targetPage !== 1 && cursorByPageRef.current[targetPage] == null) {
|
||||
return;
|
||||
}
|
||||
void loadPage(targetPage);
|
||||
},
|
||||
[loadPage, page, totalPages],
|
||||
);
|
||||
|
||||
const canGoToPage = useCallback(
|
||||
(targetPage: number) => {
|
||||
if (targetPage < 1 || targetPage > totalPages) return false;
|
||||
if (targetPage === 1 || targetPage === page) return true;
|
||||
return cursorByPageRef.current[targetPage] != null;
|
||||
},
|
||||
[page, totalPages],
|
||||
);
|
||||
|
||||
const reload = useCallback(() => {
|
||||
void loadPage(page);
|
||||
}, [loadPage, page]);
|
||||
|
||||
return {
|
||||
items,
|
||||
setItems,
|
||||
page,
|
||||
loading,
|
||||
meta,
|
||||
totalPages,
|
||||
goNext,
|
||||
goPrev,
|
||||
goToPage,
|
||||
canGoToPage,
|
||||
reload,
|
||||
refreshFromStart: () => loadPage(1, { resetStack: true }),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user